diff --git a/src/utils/companion.ts b/src/utils/companion.ts index a770a64b..c47cdefd 100644 --- a/src/utils/companion.ts +++ b/src/utils/companion.ts @@ -1,3 +1,11 @@ export const isCompanionApp = (userAgent: string): boolean => { return !!userAgent.match(/Home ?Assistant/); }; + +export const isAndroidCompanionApp = (userAgent: string): boolean => { + return !!userAgent.match(/(?=.*Home ?Assistant)(?=.*Android)/); +}; + +export const isIOSCompanionApp = (userAgent: string): boolean => { + return !!userAgent.match(/(?=.*Home ?Assistant)(?=.*iOS)/); +}; diff --git a/src/utils/download.ts b/src/utils/download.ts index cdde9bfd..5e9b77d2 100644 --- a/src/utils/download.ts +++ b/src/utils/download.ts @@ -3,7 +3,6 @@ import { localize } from '../localize/localize'; import { ExtendedHomeAssistant, FrigateCardError } from '../types'; import { ViewMedia } from '../view/media'; import { errorToConsole } from './basic'; -import { isCompanionApp } from './companion'; import { homeAssistantSignPath } from './ha'; export const downloadURL = (url: string, filename = 'download'): void => { @@ -12,23 +11,18 @@ export const downloadURL = (url: string, filename = 'download'): void => { const isSameOrigin = new URL(url).origin === window.location.origin; const dataURL = url.startsWith('data:'); - if (isCompanionApp(navigator.userAgent) || (!isSameOrigin && !dataURL)) { - // Home Assistant companion apps cannot download files without opening a - // new browser window. - // - // User-agents are specified here: - // - Android: https://github.com/home-assistant/android/blob/b285c9525dd4837a82db931c1b2321c0511494e6/common/src/main/java/io/homeassistant/companion/android/common/data/HomeAssistantApis.kt#L23 - // - iOS: https://github.com/home-assistant/iOS/blob/master/Sources/Shared/API/HAAPI.swift#L75 + if (!isSameOrigin && !dataURL) { window.open(url, '_blank'); - } else { - // Use the HTML5 download attribute to prevent a new window from - // temporarily opening. - const link = document.createElement('a'); - link.setAttribute('download', filename); - link.href = url; - link.click(); - link.remove(); + return; } + + // Use the HTML5 download attribute to prevent a new window from + // temporarily opening. + const link = document.createElement('a'); + link.setAttribute('download', filename); + link.href = url; + link.click(); + link.remove(); }; export const downloadMedia = async ( diff --git a/tests/utils/companion.test.ts b/tests/utils/companion.test.ts index ea5a11f2..740b3a31 100644 --- a/tests/utils/companion.test.ts +++ b/tests/utils/companion.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { isCompanionApp } from '../../src/utils/companion'; +import { + isCompanionApp, + isAndroidCompanionApp, + isIOSCompanionApp, +} from '../../src/utils/companion'; describe('isCompanionApp', () => { it('should return true for userAgent starting with "Home Assistant/"', () => { @@ -18,3 +22,47 @@ describe('isCompanionApp', () => { expect(isCompanionApp('')).toBe(false); }); }); + +describe('isAndroidCompanionApp', () => { + it('should return true for userAgent containing "Home Assistant" and "Android"', () => { + expect(isAndroidCompanionApp('Home Assistant/1.0 (Android 1.0; 1.0)')).toBe(true); + }); + + it('should return true for userAgent containing "HomeAssistant" and "Android"', () => { + expect(isAndroidCompanionApp('HomeAssistant/2.0 (Android 2.0; 2.0)')).toBe(true); + }); + + it('should return false for userAgent not starting with "Home Assistant/" or "HomeAssistant/"', () => { + expect(isAndroidCompanionApp('Mozilla/5.0')).toBe(false); + }); + + it('should return false for userAgent containing "Home Assistant/" or "HomeAssistant/" and iOS', () => { + expect( + isAndroidCompanionApp( + 'Home Assistant/2025.1.1 (io.robbie.HomeAssistant; build:2025.1077; iOS 18.3.0', + ), + ).toBe(false); + }); +}); + +describe('isIOSCompanionApp', () => { + it('should return true for userAgent containing "Home Assistant" and "iOS"', () => { + expect(isIOSCompanionApp('Home Assistant/1.0 (iOS 1.0; 1.0)')).toBe(true); + }); + + it('should return true for userAgent containing "HomeAssistant" and "iOS"', () => { + expect(isIOSCompanionApp('HomeAssistant/2.0 (iOS 2.0; 2.0)')).toBe(true); + }); + + it('should return false for userAgent not starting with "Home Assistant/" or "HomeAssistant/"', () => { + expect(isIOSCompanionApp('Mozilla/5.0')).toBe(false); + }); + + it('should return false for userAgent containing "Home Assistant/" or "HomeAssistant/" and Android', () => { + expect( + isIOSCompanionApp( + 'Home Assistant/2025.1.1 (io.robbie.HomeAssistant; build:2025.1077; Android 18.3.0', + ), + ).toBe(false); + }); +}); diff --git a/tests/utils/download.test.ts b/tests/utils/download.test.ts index dd4ecca9..1b4363d3 100644 --- a/tests/utils/download.test.ts +++ b/tests/utils/download.test.ts @@ -46,20 +46,16 @@ describe('downloadURL', () => { expect(link.click).toBeCalled(); }); - it('should download in apps via window.open', () => { + it('should download different origin via window.open', () => { // Set the origin to the same. const location: Location & { origin: string } = mock(); location.origin = 'http://foo'; global.window.location = location; - vi.stubGlobal('navigator', { - userAgent: 'Home Assistant/2023.3.0-3260 (Android 13; Pixel 7 Pro)', - }); - const windowSpy = vi.spyOn(window, 'open').mockReturnValue(null); - downloadURL('http://foo/url.mp4'); - expect(windowSpy).toBeCalledWith('http://foo/url.mp4', '_blank'); + downloadURL('http://bar/url.mp4'); + expect(windowSpy).toBeCalledWith('http://bar/url.mp4', '_blank'); }); });