Skip to content
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
6 changes: 6 additions & 0 deletions src/helpers/ScheduleClock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ export class ScheduleClock {
return index;
}

// A paired TV that comes back to the foreground returns to its schedule, unless it is pairing or still starting up.
static returnsToSchedule(scheduled: boolean, currentScreen: string, prevState: string, nextState: string): boolean {
if (!scheduled || !currentScreen || nextState !== "active" || !/inactive|background/.test(prevState)) return false;
return ["scheduledPlayer", "schedulePairing", "splash"].indexOf(currentScreen) === -1;
}

static jumpTo(slot: ScheduleSlot, index: number): number {
if (slot.items.length === 0) return -1;
return Math.min(Math.max(index, 0), slot.items.length - 1);
Expand Down
16 changes: 16 additions & 0 deletions src/helpers/__tests__/ScheduleClock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,19 @@ describe("ScheduleClock.showPlayer", () => {
expect(ScheduleClock.showPlayer(false, true)).toBe(false);
});
});

describe("ScheduleClock.returnsToSchedule", () => {
it("sends a paired TV that comes back to the foreground to its schedule", () => {
expect(ScheduleClock.returnsToSchedule(true, "contentBrowser", "background", "active")).toBe(true);
expect(ScheduleClock.returnsToSchedule(true, "player", "inactive", "active")).toBe(true);
});

it("leaves the screen alone when unpaired, pairing, already scheduled, or not resuming", () => {
expect(ScheduleClock.returnsToSchedule(false, "contentBrowser", "background", "active")).toBe(false);
expect(ScheduleClock.returnsToSchedule(true, "scheduledPlayer", "background", "active")).toBe(false);
expect(ScheduleClock.returnsToSchedule(true, "schedulePairing", "background", "active")).toBe(false);
expect(ScheduleClock.returnsToSchedule(true, "splash", "background", "active")).toBe(false);
expect(ScheduleClock.returnsToSchedule(true, "", "background", "active")).toBe(false);
expect(ScheduleClock.returnsToSchedule(true, "contentBrowser", "active", "background")).toBe(false);
});
});
2 changes: 1 addition & 1 deletion src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@
"paired": "Paired",
"changeTitle": "Paired to {{group}}",
"changeMessage": "Pair this screen to a different calendar, or stop following a schedule. Downloaded files for this calendar are removed.",
"keep": "Keep",
"backToSchedule": "Back to schedule",
"unpair": "Unpair",
"repair": "Pair to a different calendar"
},
Expand Down
14 changes: 12 additions & 2 deletions src/navigation/Navigator.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useEffect, useRef } from "react";
import { CachedData, Styles } from "../helpers";
import { CachedData, Styles, ScheduleClock, ScheduleSync } from "../helpers";
import { SplashScreen, PlayerScreen, SettingsScreen, PlanPairingScreen, PlanDownloadScreen, ScheduledPlayerScreen, ContentBrowserScreen, ProviderDeviceAuthScreen, ProvidersScreen, ProviderSettingsScreen, ProviderFormLoginScreen, ProviderOAuthScreen, ProviderDiscoveryScreen, ProviderDownloadScreen, DownloadsScreen } from "../screens";
import { DimensionHelper } from "../helpers/DimensionHelper";
import { View, Platform, TVEventControl, Animated } from "react-native";
import { View, Platform, TVEventControl, Animated, AppState } from "react-native";
import { NavWrapper } from "./NavWrapper";
import { OfflineScreen } from "../screens/OfflineScreen";
import PrivacyPolicyScreen from "../screens/PrivacyPolicyScreen";
Expand All @@ -13,6 +13,7 @@ export const Navigator = () => {
const [dimensions, setDimensions] = React.useState("1,1");
const [sidebarExpanded, setSidebarState] = React.useState(false);
const fadeAnim = useRef(new Animated.Value(1)).current;
const appState = useRef(AppState.currentState as string);

const handleNavigate = (page: string, data?:any) => {
if (data) setCurrentData(data); else setCurrentData(null);
Expand Down Expand Up @@ -82,6 +83,15 @@ export const Navigator = () => {
};

useEffect(init, []);

// Fire TV resumes the app on whatever screen was showing; a paired TV goes back to its schedule.
useEffect(() => {
const sub = AppState.addEventListener("change", next => {
if (ScheduleClock.returnsToSchedule(ScheduleSync.isScheduled(), CachedData.currentScreen, appState.current, next)) handleNavigate("scheduledPlayer");
appState.current = next;
});
return () => sub.remove();
}, []);
if (dimensions !== "1,1") console.log(dimensions);

const fullScreenScreens = ["splash", "player", "scheduledPlayer", "providerDownload"];
Expand Down
4 changes: 2 additions & 2 deletions src/screens/ProvidersScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Icon from "react-native-vector-icons/MaterialIcons";
import LinearGradient from "react-native-linear-gradient";
import { SvgUri } from "react-native-svg";
import { DimensionHelper } from "../helpers/DimensionHelper";
import { Styles, CachedData, ProviderAuthHelper, ProviderSettingsHelper, Colors, Typography, PlanSync } from "../helpers";
import { Styles, CachedData, ProviderAuthHelper, ProviderSettingsHelper, Colors, Typography, PlanSync, ScheduleSync } from "../helpers";
import { MenuHeader, SkeletonCard } from "../components";
import { getProvider, getAvailableProviders, FREEPLAY_PROVIDER_IDS } from "../providers";
import { ProviderInfo } from "../interfaces";
Expand Down Expand Up @@ -295,7 +295,7 @@ export const ProvidersScreen = (props: Props) => {
};

const handleBack = () => {
props.navigateTo("settings");
props.navigateTo(ScheduleSync.isScheduled() ? "scheduledPlayer" : "settings");
return true;
};

Expand Down
2 changes: 1 addition & 1 deletion src/screens/SettingsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const SettingsScreen = (props: Props) => {

const changeCalendar = () => {
Alert.alert(t("settings.calendar.changeTitle", { group: calendarValue }), t("settings.calendar.changeMessage"), [
{ text: t("settings.calendar.keep"), style: "cancel" },
{ text: t("settings.calendar.backToSchedule"), onPress: () => props.navigateTo("scheduledPlayer") },
{ text: t("settings.calendar.unpair"), style: "destructive", onPress: async () => { await ScheduleSync.unpair(); props.navigateTo("providers"); } },
{ text: t("settings.calendar.repair"), onPress: async () => { await ScheduleSync.unpair(); props.navigateTo("schedulePairing"); } }
]);
Expand Down
52 changes: 52 additions & 0 deletions src/screens/__tests__/SettingsScreen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Render SettingsScreen as a plain function call with hooks stubbed, then press the calendar row on a paired TV.
const mockScheduled = true;
const mockAlert = jest.fn();
jest.mock("react", () => ({
...jest.requireActual("react"),
useState: (init: any) => [init, () => {}],
useEffect: () => {}
}));
jest.mock("react-native", () => ({
Dimensions: { get: () => ({ width: 1920, height: 1080 }), addEventListener: () => ({ remove: () => {} }) },
Platform: { OS: "android", isTV: true, select: (o: any) => o.android ?? o.default },
StyleSheet: { create: (s: any) => s, flatten: (s: any) => s },
View: "View",
Text: "Text",
TouchableHighlight: "TouchableHighlight",
BackHandler: { addEventListener: () => ({ remove: () => {} }) },
Alert: { alert: (...args: any[]) => mockAlert(...args) }
}));
jest.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => key }) }));
jest.mock("react-native-vector-icons/MaterialIcons", () => "Icon");
jest.mock("../../helpers", () => {
const styles = jest.requireActual("../../helpers/Styles");
return {
Styles: styles.Styles,
Colors: styles.Colors,
Typography: styles.Typography,
ScheduleSync: { isScheduled: () => mockScheduled, manifest: { screenGroupName: "Room 4" }, unpair: jest.fn() }
};
});
jest.mock("../../components", () => ({ MenuHeader: "MenuHeader" }));
jest.mock("../../branding", () => ({ ...jest.requireActual("../../branding"), isLocked: false }));

import { SettingsScreen } from "../SettingsScreen";

function find(node: any, testID: string): any {
if (!node || typeof node !== "object") return null;
if (Array.isArray(node)) { for (const n of node) { const f = find(n, testID); if (f) return f; } return null; }
if (node.props?.testID === testID) return node;
return find(node.props?.children, testID);
}

describe("SettingsScreen calendar row on a paired TV", () => {
it("offers a way back to the mockScheduled player", () => {
const navigateTo = jest.fn();
const tree = SettingsScreen({ navigateTo, sidebarState: () => {} }) as any;
find(tree, "settings-calendar").props.onPress();
const buttons = mockAlert.mock.calls[0][2];
buttons[0].onPress?.();
expect(buttons[0].text).toBe("settings.calendar.backToSchedule");
expect(navigateTo).toHaveBeenCalledWith("scheduledPlayer");
});
});
Loading