From a531afd02a004f23db07b55703550a76bf779a7b Mon Sep 17 00:00:00 2001 From: Danny Date: Thu, 22 Oct 2026 15:37:20 -0700 Subject: [PATCH] Bluetooth: Coordinate companion pairing and Android Auto ownership Expose pairing readiness and coordinate adapter hand-offs with Android Auto. Show app pairings separately, support individual removal, and improve pairing controls on both comma screens. Add Galaxy companion endpoints and web asset manifests, preserve the plain camera look, and fix headless speed-limit shapes. --- openpilot/starpilot/bluetooth/native_owner.py | 71 ++++ openpilot/starpilot/bluetooth/owner.py | 11 +- .../bluetooth/tests/test_accept_pair.py | 14 + .../bluetooth/tests/test_native_owner.py | 76 ++++ .../galaxy/bin/LICENSE.hev-socks5-tunnel | 19 + .../galaxy/bin/hev-socks5-tunnel.sha256 | 1 + .../galaxy/bin/hev-socks5-tunnel_linux_arm64 | Bin 0 -> 395416 bytes openpilot/starpilot/galaxy/http_server.py | 58 ++- openpilot/starpilot/galaxy/pairing_ready.py | 51 +++ openpilot/starpilot/galaxy/server.py | 354 +++++++++++++++++- .../galaxy/test_android_auto_http.py | 7 +- .../galaxy/test_projection_bluetooth_http.py | 50 ++- .../test_projection_phone_window_http.py | 147 ++++++++ .../galaxy/tests/test_auth_runtime.py | 3 +- .../galaxy/tests/test_bluetooth_http.py | 59 +++ .../galaxy/tests/test_companion_http.py | 212 +++++++++++ .../galaxy/tests/test_http_server.py | 46 +++ .../galaxy/tests/test_pairing_ready.py | 40 ++ .../galaxy/tests/test_preview_contract.py | 3 +- .../galaxy/tests/test_remote_pairing.py | 7 +- .../galaxy/tests/test_spotify_setup.py | 251 +++++++++++++ openpilot/starpilot/galaxy/web_manifest.py | 39 ++ .../starpilot/galaxy_bluetooth/bootstrap.py | 13 +- .../galaxy_bluetooth/bridge/server.py | 16 +- .../starpilot/galaxy_bluetooth/phone_ui.py | 41 +- .../tests/integration_secretgood_http.py | 91 +++++ .../galaxy_bluetooth/tests/test_claim.py | 36 +- .../galaxy_bluetooth/tests/test_phone_ui.py | 21 ++ .../system/android_auto/bluetooth_bridge.py | 66 +++- .../starpilot/system/android_auto/daemon.py | 28 +- .../system/android_auto/projection_onroad.py | 18 +- .../starpilot/system/android_auto/protocol.py | 4 +- .../system/android_auto/source_verifier.py | 2 +- .../system/android_auto/supervisor.py | 13 + .../tests/test_bluetooth_bridge.py | 56 +++ .../tests/test_companion_bluetooth.py | 75 +++- .../android_auto/tests/test_phone_window.py | 35 +- .../tests/test_projection_plain_look.py | 21 ++ openpilot/starpilot/ui/bluetooth_compact.py | 57 ++- openpilot/starpilot/ui/bluetooth_large.py | 27 +- openpilot/starpilot/ui/settings.py | 3 + openpilot/starpilot/ui/speed_limit_shapes.py | 7 +- openpilot/starpilot/ui/speed_limit_widget.py | 2 +- .../ui/tests/test_bluetooth_compact.py | 24 +- .../ui/tests/test_bluetooth_large_pure.py | 50 ++- .../ui/tests/test_speed_limit_shapes.py | 9 + 46 files changed, 2104 insertions(+), 130 deletions(-) create mode 100644 openpilot/starpilot/bluetooth/native_owner.py create mode 100644 openpilot/starpilot/bluetooth/tests/test_native_owner.py create mode 100644 openpilot/starpilot/galaxy/bin/LICENSE.hev-socks5-tunnel create mode 100644 openpilot/starpilot/galaxy/bin/hev-socks5-tunnel.sha256 create mode 100755 openpilot/starpilot/galaxy/bin/hev-socks5-tunnel_linux_arm64 create mode 100644 openpilot/starpilot/galaxy/pairing_ready.py create mode 100644 openpilot/starpilot/galaxy/test_projection_phone_window_http.py create mode 100644 openpilot/starpilot/galaxy/tests/test_companion_http.py create mode 100644 openpilot/starpilot/galaxy/tests/test_pairing_ready.py create mode 100644 openpilot/starpilot/galaxy/tests/test_spotify_setup.py create mode 100644 openpilot/starpilot/galaxy/web_manifest.py create mode 100644 openpilot/starpilot/galaxy_bluetooth/tests/integration_secretgood_http.py create mode 100644 openpilot/starpilot/system/android_auto/tests/test_projection_plain_look.py diff --git a/openpilot/starpilot/bluetooth/native_owner.py b/openpilot/starpilot/bluetooth/native_owner.py new file mode 100644 index 0000000000..ac19a71cd8 --- /dev/null +++ b/openpilot/starpilot/bluetooth/native_owner.py @@ -0,0 +1,71 @@ +"""Native settings use Galaxy when Android Auto already owns the Bluetooth adapter.""" +from openpilot.starpilot.bluetooth.owner import AdapterBusyElsewhere, BluetoothOwner, BluetoothRejected, BluetoothUnavailable + + +class NativeBluetoothOwner(BluetoothOwner): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.galaxy = None + self.delegated_pairing = False + self.delegated_scan = False + + def _galaxy(self, operation, **kwargs): + if self.galaxy is None: + from openpilot.starpilot.galaxy_bluetooth.phone_ui import galaxy_call + self.galaxy = galaxy_call() # Keep the same authenticated session for prompts and polling. + if operation == 'status': + status, result = self.galaxy('GET', '/api/bluetooth/status') + else: + payload = {'operation': operation} + for key, value in kwargs.items(): + if key in ('address', 'enabled', 'prompt_id', 'accepted') and value is not None: + payload['promptId' if key == 'prompt_id' else key] = value + if operation == 'pairing_response': + payload['value'] = kwargs.get('value', '') + status, result = self.galaxy('POST', '/api/bluetooth/action', payload) + if status == 409: + raise BluetoothRejected('Bluetooth change was not accepted', code=result.get('code', 'busy')) + if status != 200 or not isinstance(result.get('devices'), list): + raise BluetoothUnavailable('Galaxy Bluetooth service is unavailable') + if operation in ('pair', 'accept_pair'): + self.delegated_pairing = True + elif operation == 'cancel_pair': + self.delegated_pairing = False + if operation == 'scan': + self.delegated_scan = True + elif operation in ('stop_scan', 'pair', 'accept_pair'): + self.delegated_scan = False + return result + + def request(self, operation, *, delegate=True, **kwargs): + session = kwargs.get('session') + if session is not None and not self.session_valid(session): + raise BluetoothRejected('Pairing session expired', code='session_expired') + if self.galaxy is None: + try: + return super().request(operation, **kwargs) + except AdapterBusyElsewhere: + if not delegate: + return super().snapshot(session=session) + elif not delegate: + return self.snapshot(session=session) + return self._galaxy(operation, **kwargs) + + def snapshot(self, *, session=None): + if self.galaxy is not None: + return self._galaxy('status') + return super().snapshot(session=session) + + def close(self): + try: + if self.galaxy is not None: + for operation, needed in (('cancel_pair', self.delegated_pairing), ('stop_scan', self.delegated_scan)): + if needed: + try: + self._galaxy(operation) + except (BluetoothRejected, BluetoothUnavailable, OSError, RuntimeError): + pass # The daemon also bounds scans and cancels expired pairing sessions. + finally: + self.galaxy = None + self.delegated_pairing = self.delegated_scan = False + super().close() diff --git a/openpilot/starpilot/bluetooth/owner.py b/openpilot/starpilot/bluetooth/owner.py index 41b0b28a19..03d41e266e 100644 --- a/openpilot/starpilot/bluetooth/owner.py +++ b/openpilot/starpilot/bluetooth/owner.py @@ -174,6 +174,11 @@ class BluetoothRejected(Exception): self.code = code +class AdapterBusyElsewhere(BluetoothRejected): + def __init__(self, message: str): + super().__init__(message, code='busy') + + def normalized_address(value: str) -> str: if not isinstance(value, str) or not ADDRESS.fullmatch(value.upper()): raise ValueError('Invalid Bluetooth address') @@ -577,7 +582,7 @@ class BluetoothOwner: fcntl.flock(stream, fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError: stream.close() - raise BluetoothRejected('Bluetooth adapter is busy in another process', code='busy') from None + raise AdapterBusyElsewhere('Bluetooth adapter is busy in another process') from None self.admission_file = stream def _admission_busy(self) -> bool: @@ -870,7 +875,9 @@ class BluetoothOwner: raise BluetoothRejected('Pairing changed') self.pairing.cancel() return self.snapshot(session=session) - if operation not in ('stop_scan', 'accept_pair'): + if operation != 'stop_scan': + # An unclaimed window gives way to everything, including a new window, so reopening Pair phone never + # waits out the old one. A window a phone has already claimed still blocks (below). self._end_idle_window() if self.pairing is not None and self.pairing.state == 'pairing' and operation != 'stop_scan': raise BluetoothRejected('Pairing is in progress', code='busy') diff --git a/openpilot/starpilot/bluetooth/tests/test_accept_pair.py b/openpilot/starpilot/bluetooth/tests/test_accept_pair.py index 1a0a8bb935..4999f77c9b 100644 --- a/openpilot/starpilot/bluetooth/tests/test_accept_pair.py +++ b/openpilot/starpilot/bluetooth/tests/test_accept_pair.py @@ -259,6 +259,20 @@ class AcceptPairOwnerTest(unittest.TestCase): self.assertEqual(self.paired, []) self.request_when_free('stop_scan', session=self.identity) + def test_unclaimed_window_gives_way_to_a_new_window(self): + self.request_when_free('accept_pair', session=self.identity) + self.wait_for_claim() + first = self.owner.pairing + assert first is not None + first.path = '' # no phone has claimed it + self.request_when_free('accept_pair', session=self.identity) + self.assertTrue(first.done.is_set()) + self.assertIsNot(self.owner.pairing, first) + assert self.owner.pairing is not None + self.assertEqual(self.owner.pairing.state, 'pairing') + self.request_when_free('cancel_pair', session=self.identity) + self.assertTrue(self.owner.pairing.done.wait(2)) + def test_declined_or_canceled_window_does_not_report_a_phone(self): self.owner.request('accept_pair', session=self.identity) self.wait_for_claim() diff --git a/openpilot/starpilot/bluetooth/tests/test_native_owner.py b/openpilot/starpilot/bluetooth/tests/test_native_owner.py new file mode 100644 index 0000000000..94c2b2b8d9 --- /dev/null +++ b/openpilot/starpilot/bluetooth/tests/test_native_owner.py @@ -0,0 +1,76 @@ +from unittest.mock import Mock, patch + +import pytest + +from openpilot.starpilot.bluetooth.native_owner import NativeBluetoothOwner +from openpilot.starpilot.bluetooth.owner import AdapterBusyElsewhere, BluetoothOwner, BluetoothRejected + + +@pytest.mark.parametrize('operation, fields', [ + ('scan', {}), ('stop_scan', {}), ('pair', {'address': 'AA:BB:CC:DD:EE:FF', 'session': ('large', 'one')}), + ('connect', {'address': 'AA:BB:CC:DD:EE:FF'}), ('disconnect', {'address': 'AA:BB:CC:DD:EE:FF'}), + ('forget', {'address': 'AA:BB:CC:DD:EE:FF'}), ('power', {'enabled': False}), +]) +def test_busy_native_owner_delegates_all_controls(operation, fields): + owner = NativeBluetoothOwner(lambda: True) + call = Mock(return_value=(200, {'devices': []})) + with patch.object(BluetoothOwner, 'request', side_effect=AdapterBusyElsewhere('AA owns radio')), \ + patch('openpilot.starpilot.galaxy_bluetooth.phone_ui.galaxy_call', return_value=call) as factory: + assert owner.request(operation, **fields) == {'devices': []} + payload = {'operation': operation, **{k: v for k, v in fields.items() if k != 'session'}} + call.assert_called_once_with('POST', '/api/bluetooth/action', payload) + owner.snapshot(session=('large', 'one')) + call.assert_called_with('GET', '/api/bluetooth/status') + factory.assert_called_once() + + +def test_pairing_poll_response_and_close_use_the_same_galaxy_session(): + owner = NativeBluetoothOwner(lambda: True) + call = Mock(return_value=(200, {'devices': [], 'pairing': {'state': 'pairing'}})) + with patch.object(BluetoothOwner, 'request', side_effect=AdapterBusyElsewhere('busy')) as local, \ + patch('openpilot.starpilot.galaxy_bluetooth.phone_ui.galaxy_call', return_value=call) as factory: + owner.request('pair', address='AA:BB:CC:DD:EE:FF', session=('large', 'one')) + owner.snapshot(session=('large', 'one')) + owner.request('pairing_response', prompt_id='a' * 32, accepted=True, value='', session=('large', 'one')) + call.assert_called_with('POST', '/api/bluetooth/action', + {'operation': 'pairing_response', 'promptId': 'a' * 32, 'accepted': True, 'value': ''}) + owner.close() + call.assert_called_with('POST', '/api/bluetooth/action', {'operation': 'cancel_pair'}) + local.assert_called_once() + factory.assert_called_once() + assert owner.galaxy is None + + +@pytest.mark.parametrize('error', [BluetoothRejected('park first', code='park_required'), + BluetoothRejected('Pairing is in progress', code='busy')]) +def test_local_denials_never_switch_to_galaxy(error): + owner = NativeBluetoothOwner(lambda: True) + with patch.object(BluetoothOwner, 'request', side_effect=error), \ + patch('openpilot.starpilot.galaxy_bluetooth.phone_ui.galaxy_call') as factory: + with pytest.raises(BluetoothRejected): + owner.request('scan') + factory.assert_not_called() + assert owner.galaxy is None + + +def test_delegated_page_open_never_scans(): + owner = NativeBluetoothOwner(lambda: True) + owner.galaxy = Mock(return_value=(200, {'devices': []})) + assert owner.request('scan', delegate=False) == {'devices': []} + owner.galaxy.assert_called_once_with('GET', '/api/bluetooth/status') + + +def test_denials_do_not_retry_through_a_different_owner(): + owner = NativeBluetoothOwner(lambda: True) + with patch.object(BluetoothOwner, 'request', side_effect=BluetoothRejected('park first', code='park_required')), \ + patch('openpilot.starpilot.galaxy_bluetooth.phone_ui.galaxy_call') as factory: + with pytest.raises(BluetoothRejected, match='park first'): + owner.request('power', enabled=False) + factory.assert_not_called() + call = Mock(return_value=(409, {'code': 'busy'})) + owner.galaxy = call + with patch.object(BluetoothOwner, 'request') as local: + with pytest.raises(BluetoothRejected): + owner.request('scan') + local.assert_not_called() + call.assert_called_once() diff --git a/openpilot/starpilot/galaxy/bin/LICENSE.hev-socks5-tunnel b/openpilot/starpilot/galaxy/bin/LICENSE.hev-socks5-tunnel new file mode 100644 index 0000000000..60a7d088b6 --- /dev/null +++ b/openpilot/starpilot/galaxy/bin/LICENSE.hev-socks5-tunnel @@ -0,0 +1,19 @@ +Copyright (c) 2022 hev + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/openpilot/starpilot/galaxy/bin/hev-socks5-tunnel.sha256 b/openpilot/starpilot/galaxy/bin/hev-socks5-tunnel.sha256 new file mode 100644 index 0000000000..d8907e75c9 --- /dev/null +++ b/openpilot/starpilot/galaxy/bin/hev-socks5-tunnel.sha256 @@ -0,0 +1 @@ +e44a6965cd718d98829f344e9f300a5664d12020557127b7469bee7c8f3098b0 hev-socks5-tunnel_linux_arm64 diff --git a/openpilot/starpilot/galaxy/bin/hev-socks5-tunnel_linux_arm64 b/openpilot/starpilot/galaxy/bin/hev-socks5-tunnel_linux_arm64 new file mode 100755 index 0000000000000000000000000000000000000000..e3f8974530f8048e6aa6c04aec59e120969bd5ea GIT binary patch literal 395416 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%0Douyhpd_ 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 lMLiru6722MMmk)%0u|qL(=_0d|wIAvXA3bBf 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 bool: + if getattr(self, 'remote_transport', False): + return False + try: + return request.getpeername()[0] == request.getsockname()[0] == '127.0.0.1' + except (OSError, IndexError, TypeError): + return False + + def _admit(self, request) -> threading.BoundedSemaphore | None: + """The pool this request got a slot from; loopback overflows into its reserve instead of waiting.""" + if self._draining: + return None + if self.loopback(request): + if self._slots.acquire(blocking=False): + return self._slots + return self._loopback_slots if self._loopback_slots.acquire(timeout=self.REQUEST_TIMEOUT) else None + return self._slots if self._slots.acquire(timeout=self.REQUEST_TIMEOUT) else None + + def process_request_thread(self, request, client_address): + # Wait on the request's own thread so a full browser pool never blocks accepting. + slots = self._admit(request) + if slots is None: self.shutdown_request(request) return try: - super().process_request(request, client_address) - except Exception: - self._slots.release() - raise - - def process_request_thread(self, request, client_address): - try: - super().process_request_thread(request, client_address) + if self._draining: + self.shutdown_request(request) + else: + super().process_request_thread(request, client_address) finally: - self._slots.release() + slots.release() def drain_requests(self, timeout: float) -> bool: """Wait for accepted requests before closing their shared data sources.""" + self._draining = True deadline = time.monotonic() + timeout - acquired = 0 + acquired: list[threading.BoundedSemaphore] = [] try: - for _ in range(self.MAX_CONNECTIONS): - if not self._slots.acquire(timeout=max(0.0, deadline - time.monotonic())): - return False - acquired += 1 + for slots, count in ((self._slots, self.MAX_CONNECTIONS), (self._loopback_slots, self.LOOPBACK_CONNECTIONS)): + for _ in range(count): + if not slots.acquire(timeout=max(0.0, deadline - time.monotonic())): + return False + acquired.append(slots) return True finally: - for _ in range(acquired): - self._slots.release() + for slots in acquired: + slots.release() def service_actions(self): super().service_actions() diff --git a/openpilot/starpilot/galaxy/pairing_ready.py b/openpilot/starpilot/galaxy/pairing_ready.py new file mode 100644 index 0000000000..6f14f7823b --- /dev/null +++ b/openpilot/starpilot/galaxy/pairing_ready.py @@ -0,0 +1,51 @@ +"""Whether comma can take a phone's Bluetooth pairing right now, so companion apps wait instead of failing.""" +from collections.abc import Callable + +# Short-lived states a phone may wait out; the others need someone to do something on comma. +TRANSIENT = frozenset(('starting', 'bridge_starting')) +BOOT_GRACE_SECONDS = 45.0 + + +def system_uptime() -> float: + try: + with open('/proc/uptime') as stream: + return float(stream.read().split()[0]) + except (OSError, ValueError, IndexError): + return BOOT_GRACE_SECONDS # no grace where the uptime cannot be read (not a comma) + + +def bridge_state() -> tuple[str, str]: + """The phone bridge's (state, message); 'pending' whenever it cannot be read.""" + try: + from openpilot.starpilot.galaxy_bluetooth.bootstrap import setup_state + return setup_state() + except Exception: + return 'pending', 'Bridge startup pending' + + +def pairing_readiness(*, parked: Callable[[], bool], snapshot: Callable[[], dict], + bridge: Callable[[], tuple[str, str]], uptime: Callable[[], float] = system_uptime) -> dict: + """{'ready': bool, 'code': str, 'message': str}; checked in the order a person would fix them.""" + def answer(ready: bool, code: str, message: str) -> dict: + return {'schema': 1, 'ready': ready, 'code': code, 'message': message} + + if uptime() < BOOT_GRACE_SECONDS: + return answer(False, 'starting', 'Comma just started and Bluetooth is still getting ready.') + try: + is_parked = parked() + state = snapshot() if is_parked else {} + except Exception: # a readiness question must always get an answer, never an error + return answer(False, 'bluetooth_unavailable', 'Comma’s Bluetooth service is not responding.') + if not is_parked: + return answer(False, 'park_required', 'Park the car or turn it off to pair a phone.') + if state.get('errorCode') or not state.get('available', True): + return answer(False, 'bluetooth_unavailable', 'Comma’s Bluetooth adapter is not available.') + if not state.get('powered'): + return answer(False, 'bluetooth_off', 'Bluetooth is off on comma. Turn it on in Settings > Bluetooth.') + try: + bridge_state, bridge_message = bridge() + except Exception: + bridge_state, bridge_message = 'pending', '' + if bridge_state != 'ready': + return answer(False, 'bridge_starting', bridge_message or 'The phone Bluetooth bridge is starting.') + return answer(True, 'ready', 'Ready to pair.') diff --git a/openpilot/starpilot/galaxy/server.py b/openpilot/starpilot/galaxy/server.py index e1e95ff019..68e174f2f6 100644 --- a/openpilot/starpilot/galaxy/server.py +++ b/openpilot/starpilot/galaxy/server.py @@ -5,6 +5,7 @@ import hashlib from contextlib import nullcontext from http.cookies import SimpleCookie import json +import os import re import secrets import select @@ -14,11 +15,12 @@ import threading import time from urllib.parse import parse_qs, unquote, urlsplit +from openpilot.common.swaglog import cloudlog from openpilot.starpilot.galaxy.batch_routes import change_drive_state, delete_records, non_default_settings, preview_layout, preview_settings_recovery from openpilot.starpilot.galaxy.access import AccessStatus, default_owner from openpilot.starpilot.galaxy.auth import LocalSessions from openpilot.starpilot.galaxy.http_server import (_LocalHTTPHandler, _LocalHTTPServer, allowed_authority, - direct_local_connection, static_asset) + direct_local_connection, install_origin_allowed, static_asset) from openpilot.starpilot.galaxy.camera_snapshot import CameraSnapshot, SnapshotDenied, SnapshotUnavailable from openpilot.starpilot.galaxy.remote import default_remote_pairing, gateway_cookie_valid from openpilot.starpilot.galaxy.crash_reports import CrashChanged, CrashMissing, CrashReports, CrashUnavailable @@ -39,6 +41,7 @@ from openpilot.starpilot.galaxy.map_status import MapStatus, MapUnavailable from openpilot.starpilot.galaxy.map_operations import MapOperationError, MapOperations, validate_action from openpilot.starpilot.galaxy.plots import Plots from openpilot.starpilot.galaxy.software_status import SoftwareStatus, SoftwareUnavailable +from openpilot.starpilot.galaxy.web_manifest import web_manifest from openpilot.starpilot.galaxy.software_operations import SoftwareOperations, SoftwareOperationError from openpilot.starpilot.galaxy.settings import PAGES, SettingsGateway, SettingsChanged, SettingsUnavailable from openpilot.starpilot.galaxy.onroad_layout import LayoutChanged, OnroadLayoutOwner @@ -59,6 +62,34 @@ from openpilot.starpilot.audio.downloads import SoundDownloads, SoundDownloadErr WEB = Path(__file__).resolve().parent / 'web' INSTANCE = secrets.token_hex(8) + + +def phone_claim_view(): + from openpilot.starpilot.galaxy_bluetooth.bridge import claim + return claim.view(claim.CLAIM_PATH) + + +def link_music_phone(address): + """Music paired in the window an app claimed (either order): remember which device is the app's phone.""" + try: + from openpilot.starpilot.galaxy_bluetooth.bridge import claim, pairing + claim.note_paired(address) + linked = claim.take_link() + if linked: + pairing.link_address(*linked) + except (OSError, ValueError): + pass + + +def forget_app_keys(address): + """Forgetting a Bluetooth device also forgets the app key that was paired with it.""" + try: + from openpilot.starpilot.galaxy_bluetooth.bridge import pairing + pairing.remove_by_address(address) + except (OSError, ValueError): + pass + + def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashes=None, software=None, maps=None, models=None, settings=None, plots=None, map_operations=None, recordings=None, recording_media=None, segment_summary=None, sentry_events=None, notifications=None, training_results=None, bluetooth=None, vehicle_selection=None, @@ -66,8 +97,13 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe sounds=None, software_operations=None, drive_stats=None, layout_preview_socket=None, controllers_socket=None, remote_pairing=None, parked=None, camera_snapshot=None, clock=time.monotonic, android_auto_setup=None, android_auto_client=None, navigation=None, offline_maps=None, drive_state=None, cloud_provider=None, cloud_offroad=None, projection_layout=None, - local_access=None, tmux_live=None, android_auto_logs=None, hotspot=None, toyota_tools=None, replay_client=None, replay_root=None): + local_access=None, tmux_live=None, android_auto_logs=None, hotspot=None, toyota_tools=None, replay_client=None, replay_root=None, + spotify_setup=None, companion=None, phone_location=None): + from openpilot.starpilot.galaxy.companion import CompanionRegistry, APK_PATH + from openpilot.starpilot.gps.phone import PhoneGpsPublisher + companion_source = companion if companion is not None else CompanionRegistry() training_results_source = training_results if training_results is not None else FlmTrainingResults() + phone_location_source = phone_location if phone_location is not None else PhoneGpsPublisher() cloud = cloud_provider hotspot_source = hotspot hotspot_source_lock = threading.Lock() @@ -175,6 +211,7 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe bluetooth_lock = threading.Lock() bluetooth_source = bluetooth aa_setup_source = android_auto_setup + spotify_setup_source = spotify_setup aa_client_source = android_auto_client aa_source_lock = threading.Lock() aa_registry_source = None @@ -304,13 +341,18 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe return record is not None and generation == remote_generation(current, record) and \ (sessions.valid(token, generation) or gateway_cookie_valid(token, record)) + def bluetooth_paired(address, name, uuids): + link_music_phone(address) + spotify_owner().adopt_phone(address, name, uuids) + def bluetooth_owner(): nonlocal bluetooth_source with bluetooth_lock: if bluetooth_source is None: from openpilot.common.params import Params authority = context_source(Params()) - bluetooth_source = BluetoothOwner(authority.configuration_allowed, session_valid=bluetooth_session_valid) + bluetooth_source = BluetoothOwner(authority.configuration_allowed, session_valid=bluetooth_session_valid, + on_paired=bluetooth_paired) server.bluetooth_authority = authority server.bluetooth_source = bluetooth_source return bluetooth_source @@ -343,6 +385,18 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe server.android_auto_setup_source = aa_setup_source return aa_setup_source + def spotify_owner(): + nonlocal spotify_setup_source + with bluetooth_lock: + if spotify_setup_source is None: + from openpilot.starpilot.galaxy.spotify_setup import SpotifySetup + def paired_devices(identity): + return bluetooth_owner().snapshot(session=identity).get('devices', []) + spotify_setup_source = SpotifySetup(parked=configuration_allowed, session_valid=bluetooth_session_valid, + paired_devices=paired_devices) + server.spotify_setup_source = spotify_setup_source + return spotify_setup_source + def aa_source_registry(): nonlocal aa_registry_source with aa_source_lock: @@ -389,7 +443,7 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe if settings_gateway is None: from openpilot.common.params import Params params = Params() - settings_gateway = SettingsGateway(params, context_source(params)) + settings_gateway = SettingsGateway(params, context_source(params), android_auto=aa_client) server.settings_source = settings_gateway return settings_gateway @@ -515,6 +569,10 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe return not getattr(self.server, 'remote_transport', False) and \ direct_local_connection(self.client_address[0], self.connection.getsockname()[0], self.headers) + def spotify_install_allowed(self): + return install_origin_allowed(self.client_address[0], self.connection.getsockname()[0], self.headers, + getattr(self.server, 'remote_transport', False)) + def authenticated(self): return self.settings_session() is not None @@ -564,6 +622,51 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe finally: aa_source_registry().revoke_source(source) + def phone_claim(self, operation, result): + """The same window that lets a phone pair for music also lets the StarPilot app claim its key.""" + from openpilot.starpilot.galaxy_bluetooth.bridge import claim + try: + if operation == 'cancel_pair': + claim.clear() + elif type(result) is dict: + claim.arm(result.get('adapterAddress', ''), result.get('adapterName', '')) + except OSError: + pass + + def aa_phone_window(self, identity, operation, payload=None): + """The phone pairing window inside a running Android Auto projection; handled False when none holds the adapter.""" + registry = aa_source_registry() + minted = operation in ('accept_pair', 'pair', 'scan', 'stop_scan') + source = registry.mint(identity) if minted else registry.current(identity) + if source is None: + return {'handled': False} + arguments = {'address': payload['address']} if operation == 'pair' and payload is not None else {} + if operation == 'pairing_response' and payload is not None: + arguments = {'prompt_id': payload['promptId'], 'accepted': payload['accepted'], 'value': payload.get('value', '')} + try: + if self.settings_session() != identity: + raise ValueError('Galaxy session changed') + delegated = aa_client().call('phone_window', source=source, operation=operation, **arguments) + except (OSError, RuntimeError) as error: + # Falling back to this process cannot work while Android Auto holds the adapter, so say why it was skipped. + cloudlog.warning(f'android auto phone window {operation} not delegated: {error!r}') + delegated = {'handled': False} + if type(delegated) is not dict or type(delegated.get('handled')) is not bool: + delegated = {'handled': False} + if minted and (not delegated['handled'] or 'error_code' in delegated or operation in ('scan', 'stop_scan')) or \ + operation == 'cancel_pair' and delegated['handled']: + registry.revoke_source(source) + found = delegated.get('bluetooth') + paired = found.pop('windowPaired', None) if type(found) is dict else None + if type(paired) is dict: + link_music_phone(paired.get('address', '')) + if type(paired) is dict and configuration_allowed(): + try: + spotify_owner().adopt_phone(paired['address'], paired['name'], tuple(paired['uuids'])) + except Exception: + pass + return delegated + def do_GET(self): if not self.local_request(): return @@ -626,6 +729,33 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe else: if self.require_session(): self.json(200, result) + elif path in ('/app.apk', '/api/companion/app.apk'): + if getattr(self.server, 'remote_transport', False): + self.json(403, {'error': 'Download the app over local Wi-Fi'}) + return + if not self.direct_local() and not self.require_session(): + return + try: + handle = APK_PATH.open('rb') + except OSError: + self.json(404, {'error': 'Companion APK is not installed'}) + return + with handle: + self.send_response(200) + self.send_header('Content-Type', 'application/vnd.android.package-archive') + self.send_header('Content-Length', str(os.fstat(handle.fileno()).st_size)) + self.send_header('Content-Disposition', 'attachment; filename="starpilot-companion.apk"') + self.send_header('Cache-Control', 'no-store') + self.end_headers() + if self.command != 'HEAD': + while chunk := handle.read(65536): + self.wfile.write(chunk) + elif path == '/api/companion/status': + if self.require_session(): + try: + self.json(200, companion_source.snapshot()) + except (OSError, ValueError): + self.json(503, {'error': 'Companion settings unavailable'}) elif path == '/api/galaxy/hotspot': if not self.require_session(): return @@ -648,15 +778,16 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe 'tunnelClientAvailable': frpc_binary() is not None, 'legacyPassword': access.status().status == AccessStatus.LEGACY_IMPORT_AVAILABLE, 'legacyPairingAvailable': legacy_available is True}) - elif path == '/api/galaxy/qr.svg': + elif path in ('/api/galaxy/qr.svg', '/api/companion/qr.svg'): if not self.require_session(): return record = pairing.read() - if record is None: + if record is None and path == '/api/galaxy/qr.svg': self.json(404, {'error': 'Galaxy is not paired'}) return from openpilot.common.qrcode import _Qr, _capacity - raw = pairing.url(record['slug']).encode() + from openpilot.starpilot.galaxy.hotspot import ADDRESS + raw = (f"http://{ADDRESS}:8082/app.apk" if path == '/api/companion/qr.svg' or record is None else pairing.url(record['slug'])).encode() version = next(version for version in range(1, 21) if 4 + 8 + len(raw) * 8 <= _capacity(version) * 8) matrix = _Qr(version, raw).modules @@ -666,8 +797,15 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe svg = (f'' + f'{cells}') self.respond(200, svg.encode(), 'image/svg+xml') - elif path == '/data/runtime.json': + elif path in ('/data/runtime.json', '/api/companion/runtime'): self.json(200, {'schemaVersion': 1, 'monitor': 'local', 'instance': INSTANCE}) + elif path == '/api/companion/web-manifest': + self.json(200, web_manifest(WEB)) + elif path == '/api/companion/bluetooth-ready': + if not self.require_session(): + return + from openpilot.starpilot.galaxy.pairing_ready import bridge_state, pairing_readiness + self.json(200, pairing_readiness(parked=configuration_allowed, snapshot=lambda: bluetooth_owner().snapshot(), bridge=bridge_state)) elif path in ('/api/local-access', '/api/tmux/live', '/api/troubleshoot'): if not self.require_session(): return @@ -821,6 +959,11 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe return try: result = bluetooth_owner().snapshot(session=identity) + result['claim'] = phone_claim_view() + if result.get('pairing') is None and aa_setup_owner().enabled(): + delegated = self.aa_phone_window(identity, 'status') + if delegated['handled'] and type(delegated.get('bluetooth')) is dict: + result['pairing'] = delegated['bluetooth'].get('pairing') except (OSError, RuntimeError): self.json(503, {'error': 'Bluetooth is unavailable', 'code': 'unavailable'}) else: @@ -844,6 +987,22 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe self.json(200, result) else: self.json(401, {'error': 'Sign in to Galaxy'}) + elif path == '/api/spotify/setup': + if not self.require_session(): + return + identity = self.settings_session() + if identity is None: + self.json(401, {'error': 'Sign in to Galaxy'}) + return + try: + result = {**spotify_owner().status(identity), 'uploadAllowed': self.spotify_install_allowed()} + except Exception: + self.json(503, {'error': 'Spotify setup is unavailable'}) + else: + if self.settings_session() == identity: + self.json(200, result) + else: + self.json(401, {'error': 'Sign in to Galaxy'}) elif path == '/api/android-auto/pairing/status': if not self.require_session(): return @@ -1352,12 +1511,52 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe finally: self.connection.settimeout(old_timeout) return + if path == '/api/spotify/upload': + if not self.require_session(): + return + identity = self.settings_session() + if identity is None: + self.json(401, {'error': 'Sign in to Galaxy'}) + return + if not self.spotify_install_allowed(): + self.json(403, {'error': 'Install Spotify over your local network or the Galaxy tunnel'}) + return + if self.headers.get('Transfer-Encoding') is not None or \ + self.headers.get('Content-Type', '').lower() != 'application/octet-stream': + self.json(415, {'error': 'Binary .tar.gz upload required'}) + return + try: + size = int(self.headers.get('Content-Length', '')) + except ValueError: + size = -1 + from openpilot.starpilot.soloist.package import MAX_UPLOAD_BYTES + if not 0 < size <= MAX_UPLOAD_BYTES: + self.json(413, {'error': 'Soloist download size is invalid'}) + return + from openpilot.starpilot.galaxy.spotify_setup import SetupRejected as SpotifyRejected + old_timeout = self.connection.gettimeout() + try: + self.connection.settimeout(30.0) + result = spotify_owner().upload(identity, self.rfile, size) + except SpotifyRejected as error: + self.json(409, {'error': str(error)}) + except (OSError, TimeoutError): + self.json(503, {'error': 'Soloist upload failed'}) + else: + if self.settings_session() == identity: + self.json(202, result) + else: + self.json(401, {'error': 'Sign in to Galaxy'}) + finally: + self.connection.settimeout(old_timeout) + return if path.startswith('/api/flm/'): self.close_connection = True if self.require_session(): self.json(404, {'error': 'Not found'}) return - if path not in ('/api/connect/provider', '/api/auth/login', '/api/auth/logout', '/api/settings/preview', '/api/settings/confirm', + if path not in ('/api/companion/uplink', '/api/companion/settings', '/api/companion/location', + '/api/connect/provider', '/api/auth/login', '/api/auth/logout', '/api/settings/preview', '/api/settings/confirm', '/api/settings/recovery/preview', '/api/settings/recovery/confirm', '/api/settings/reset-default', '/api/settings/restore', '/api/recordings/action', '/api/replay/start', '/api/replay/control', '/api/replay/stop', @@ -1365,6 +1564,7 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe '/api/galaxy/pair', '/api/galaxy/unpair', '/api/galaxy/device-name', '/api/galaxy/hotspot', '/api/cameras/snapshot', '/api/android-auto/layout', '/api/android-auto/enable', '/api/android-auto/control', '/api/android-auto/pairing', '/api/android-auto/pairing/response', '/api/android-auto/pairing/cancel', '/api/android-auto/pairing/select', + '/api/spotify/enable', '/api/spotify/key', '/api/spotify/preferences', '/api/spotify/control', '/api/spotify/maintenance', '/api/ui/layout', '/api/ui/layout/preview', '/api/favorites/slots', '/api/maps/start', '/api/maps/cancel', '/api/bluetooth/action', '/api/controllers/action', @@ -1377,8 +1577,9 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe '/api/toyota-tools/action', '/api/vehicle-selection/preview', '/api/vehicle-selection/confirm'): self.json(405, {'error': 'Method unavailable'}) return - if (path.startswith(('/api/replay/', '/api/recordings/', '/api/connect/', '/api/cameras/', '/api/settings/', '/api/maps/', '/api/bluetooth/', - '/api/android-auto/', '/api/controllers/', '/api/vehicle-selection/', '/api/toyota-tools/', + if (path.startswith(('/api/companion/', '/api/replay/', '/api/recordings/', '/api/connect/', '/api/cameras/', '/api/settings/', '/api/maps/', + '/api/bluetooth/', + '/api/android-auto/', '/api/spotify/', '/api/controllers/', '/api/vehicle-selection/', '/api/toyota-tools/', '/api/models/', '/api/ui/', '/api/favorites/', '/api/sounds/', '/api/software/', '/api/drives/', '/api/navigation/', '/api/drive-state/', '/api/sentry/', '/api/crash-reports/')) and not self.require_session()): @@ -1411,6 +1612,33 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe if path.startswith('/api/replay/'): replay_api.post(self, path, payload) return + if path == '/api/companion/location': + if getattr(self.server, 'remote_transport', False) or not self.direct_local(): + self.json(403, {'error': 'Phone location requires a direct local connection'}) + return + try: + self.json(200, phone_location_source.publish(payload)) + except ValueError as error: + self.json(400, {'error': str(error)}) + return + if path in ('/api/companion/uplink', '/api/companion/settings'): + if getattr(self.server, 'remote_transport', False) or not self.direct_local(): + self.json(403, {'error': 'Companion controls require a direct local connection'}) + return + try: + result = (companion_source.heartbeat(payload, self.client_address[0]) if path.endswith('/uplink') + else companion_source.save(payload)) + except PermissionError as error: + self.json(403, {'error': str(error)}) + except FileExistsError as error: + self.json(409, {'error': str(error)}) + except ValueError as error: + self.json(400, {'error': str(error)}) + except OSError: + self.json(503, {'error': 'Companion settings unavailable'}) + else: + self.json(200, result) + return if path == '/api/galaxy/hotspot': identity = self.settings_session() if identity is None: @@ -1949,6 +2177,37 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe aa_registry_source.revoke(identity) self.respond(200, b'{"authenticated":false}', cookie='galaxy_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0') return + if path.startswith('/api/spotify/'): + identity = self.settings_session() + if identity is None: + self.json(401, {'error': 'Sign in to Galaxy'}) + return + from openpilot.starpilot.galaxy.spotify_setup import SetupRejected as SpotifyRejected + try: + owner = spotify_owner() + if path == '/api/spotify/enable' and type(payload) is dict and set(payload) == {'enabled'}: + result = owner.enable(identity, payload['enabled']) + elif path == '/api/spotify/key' and type(payload) is dict and set(payload) == {'key'}: + result = owner.set_key(identity, payload['key']) + elif path == '/api/spotify/preferences' and type(payload) is dict: + result = owner.save_preferences(identity, payload) + elif path == '/api/spotify/control' and type(payload) is dict and set(payload) in ({'action'}, {'action', 'args'}): + result = owner.control(identity, payload['action'], payload.get('args', {})) + elif path == '/api/spotify/maintenance' and type(payload) is dict and set(payload) == {'action'}: + result = owner.maintenance(identity, payload['action']) + else: + self.json(400, {'error': 'Invalid Spotify request'}) + return + except SpotifyRejected as error: + self.json(409, {'error': str(error)}) + except (OSError, RuntimeError): + self.json(503, {'error': 'Spotify is unavailable'}) + else: + if self.settings_session() == identity: + self.json(200, result) + else: + self.json(401, {'error': 'Sign in to Galaxy'}) + return if path == '/api/android-auto/enable': if type(payload) is not dict or set(payload) not in ({'enabled'}, {'enabled', 'enableBluetooth'}) or \ type(payload['enabled']) is not bool or type(payload.get('enableBluetooth', False)) is not bool or \ @@ -2008,7 +2267,8 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe self.json(403, {'error': 'Park before changing automatic connection'}) return command, arguments = 'set_auto_connect', {'enabled': payload['enabled']} - elif action == 'connection' and set(payload) == {'action', 'connection'} and payload['connection'] in ('wireless', 'wired'): + elif (action == 'connection' and set(payload) == {'action', 'connection'} and + payload['connection'] in ('wireless', 'wired', 'headunitpad', 'openheadunit')): if not configuration_allowed(): self.json(403, {'error': 'Park before changing the connection type'}) return @@ -2030,7 +2290,7 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe runtime = aa_client().call('status')['status'] if runtime.get('pairing_ready'): raise ValueError('Finish or cancel pairing first') - if command == 'start' and not runtime.get('receiver_address') and runtime.get('connection') != 'wired': + if command == 'start' and not runtime.get('receiver_address') and runtime.get('connection') == 'wireless': raise ValueError('Select a car first') if command == 'start' and not aa_setup_owner().identity_status().get('installed'): raise ValueError('Verify your Android Auto package first') @@ -2220,7 +2480,7 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe if payload['operation'] == 'power': if set(payload) != {'operation', 'enabled'} or type(payload['enabled']) is not bool: raise ValueError('Invalid Bluetooth request') - elif payload['operation'] in ('scan', 'stop_scan', 'cancel_pair'): + elif payload['operation'] in ('scan', 'stop_scan', 'cancel_pair', 'accept_pair'): if set(payload) != {'operation'}: raise ValueError('Invalid Bluetooth request') elif payload['operation'] == 'pairing_response': @@ -2238,18 +2498,50 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe self.json(401, {'error': 'Sign in to Galaxy'}) return delegated = {'handled': False} - if payload['operation'] in ('connect', 'disconnect') and aa_setup_owner().enabled(): - delegated = self.aa_command(identity, 'bluetooth_action', operation=payload['operation'], - address=normalized_address(payload['address'])) + if payload['operation'] == 'accept_pair' and configuration_allowed(): + spotify_owner().prepare_phone_pairing() + if payload['operation'] in ('connect', 'disconnect', 'forget') and aa_setup_owner().enabled(): + try: + delegated = self.aa_command(identity, 'bluetooth_action', operation=payload['operation'], + address=normalized_address(payload['address'])) + except (OSError, RuntimeError): + # Without a running Android Auto service nothing else holds the adapter, so forgetting is local. + if payload['operation'] != 'forget': + raise + delegated = {'handled': False} if type(delegated) is not dict or type(delegated.get('handled')) is not bool: raise BluetoothUnavailable('service_unavailable') if 'error_code' in delegated: code = delegated['error_code'] - if type(code) is str and code in ('busy', 'park_required', 'changed', 'session_expired'): + if type(code) is str and code in ('busy', 'park_required', 'changed', 'session_expired', 'projection_active'): raise BluetoothRejected('Android Auto could not complete the Bluetooth change', code=code) unavailable = ('service_unavailable', 'adapter_unavailable', 'radio_unavailable', 'radio_preference_unavailable') raise BluetoothUnavailable('Android Auto Bluetooth service is unavailable', code=code if type(code) is str and code in unavailable else 'service_unavailable') + elif payload['operation'] == 'power' and aa_setup_owner().enabled(): + # Android Auto may hold the adapter in its own process; it releases that hold before the radio stops. + try: + delegated = self.aa_command(identity, 'bluetooth_power', enabled=payload['enabled']) + except (OSError, RuntimeError): + delegated = {'handled': False} + if type(delegated) is not dict or type(delegated.get('handled')) is not bool: + delegated = {'handled': False} + if 'error_code' in delegated: + code = delegated['error_code'] + if type(code) is str and code in ('busy', 'park_required', 'changed', 'session_expired', 'projection_active'): + why = delegated.get('message') + raise BluetoothRejected(f'Android Auto: {why}' if type(why) is str and why else + 'Android Auto could not complete the Bluetooth change', code=code) + raise BluetoothUnavailable('Android Auto Bluetooth service is unavailable') + elif payload['operation'] in ('accept_pair', 'pair', 'scan', 'stop_scan', 'pairing_response', 'cancel_pair') and aa_setup_owner().enabled(): + delegated = self.aa_phone_window(identity, payload['operation'], payload) + if 'error_code' in delegated: + code = delegated['error_code'] + if type(code) is str and code in ('busy', 'park_required', 'changed', 'session_expired'): + why = delegated.get('message') + raise BluetoothRejected(f'Android Auto: {why}' if type(why) is str and why else + 'Android Auto could not complete the Bluetooth change', code=code) + raise BluetoothUnavailable('Android Auto Bluetooth service is unavailable') if delegated['handled']: result = delegated.get('bluetooth') if type(result) is not dict: @@ -2258,13 +2550,19 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe result = bluetooth_owner().request(payload['operation'], address=payload.get('address'), enabled=payload.get('enabled'), session=identity, prompt_id=payload.get('promptId'), accepted=payload.get('accepted'), value=payload.get('value', '')) + if payload['operation'] in ('accept_pair', 'cancel_pair'): + self.phone_claim(payload['operation'], result) + elif payload['operation'] == 'forget': + forget_app_keys(payload['address']) except ValueError: if self.settings_session() == identity: self.json(400, {'error': 'Invalid Bluetooth request'}) except BluetoothRejected as error: + cloudlog.warning(f'bluetooth {payload.get("operation")} refused ({error.code}): {error}') if self.settings_session() == identity: self.json(409, {'error': 'Bluetooth change was not accepted', 'code': error.code}) except BluetoothUnavailable as error: + cloudlog.warning(f'bluetooth {payload.get("operation")} unavailable ({error.code}): {error}') if self.settings_session() == identity: self.json(503, {'error': 'Bluetooth service could not complete the request', 'code': error.code}) except (OSError, RuntimeError): @@ -2426,6 +2724,26 @@ def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashe def do_DELETE(self): if not self.local_request(mutation=True): return + if urlsplit(self.path).path == '/api/spotify': + if not self.require_session(): + return + identity = self.settings_session() + if identity is None: + self.json(401, {'error': 'Sign in to Galaxy'}) + return + from openpilot.starpilot.galaxy.spotify_setup import SetupRejected as SpotifyRejected + try: + result = spotify_owner().remove(identity) + except SpotifyRejected as error: + self.json(409, {'error': str(error)}) + except (OSError, RuntimeError): + self.json(503, {'error': 'Spotify data could not be removed'}) + else: + if self.settings_session() == identity: + self.json(200, result) + else: + self.json(401, {'error': 'Sign in to Galaxy'}) + return if urlsplit(self.path).path != '/api/android-auto/identity': self.json(405, {'error': 'Method unavailable'}) return diff --git a/openpilot/starpilot/galaxy/test_android_auto_http.py b/openpilot/starpilot/galaxy/test_android_auto_http.py index a3b4b7997c..a32896d7b1 100644 --- a/openpilot/starpilot/galaxy/test_android_auto_http.py +++ b/openpilot/starpilot/galaxy/test_android_auto_http.py @@ -403,18 +403,19 @@ def test_existing_paired_car_controls_are_local_authenticated_and_source_bound(a cookie=cookie, content_type='application/json')[0] == 401 -def test_wired_connection_requires_park_but_not_a_paired_receiver(api): +@pytest.mark.parametrize('connection', ['wired', 'headunitpad', 'openheadunit']) +def test_wired_connection_requires_park_but_not_a_paired_receiver(api, connection): request, _, state, client = api code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}), content_type='application/json') assert code == 200 cookie = headers['Set-Cookie'].split(';', 1)[0] - payload = json.dumps({'action': 'connection', 'connection': 'wired'}) + payload = json.dumps({'action': 'connection', 'connection': connection}) state['parked'] = False assert request('POST', '/api/android-auto/control', payload, cookie=cookie, content_type='application/json')[0] == 403 state['parked'] = True code, result, _ = request('POST', '/api/android-auto/control', payload, cookie=cookie, content_type='application/json') - assert code == 200 and result['status']['connection'] == 'wired' + assert code == 200 and result['status']['connection'] == connection assert request('POST', '/api/android-auto/control', json.dumps({'action': 'start'}), cookie=cookie, content_type='application/json')[0] == 200 assert client.running diff --git a/openpilot/starpilot/galaxy/test_projection_bluetooth_http.py b/openpilot/starpilot/galaxy/test_projection_bluetooth_http.py index 450025f077..18a9ed4f75 100644 --- a/openpilot/starpilot/galaxy/test_projection_bluetooth_http.py +++ b/openpilot/starpilot/galaxy/test_projection_bluetooth_http.py @@ -48,8 +48,8 @@ class AA: self.http = None def call(self, command, **kwargs): self.calls.append((command, kwargs)) - assert command == 'bluetooth_action' - assert set(kwargs) == {'source', 'operation', 'address'} + assert command in ('bluetooth_action', 'bluetooth_power') + assert set(kwargs) == ({'source', 'operation', 'address'} if command == 'bluetooth_action' else {'source', 'enabled'}) assert self.http is not None code, data, _ = self.http('/api/android-auto/source/' + kwargs['source']) assert code == 200 and data == {'valid': True}, 'Real minted source proof was not live' @@ -113,6 +113,37 @@ class ProjectionBluetoothHttpTest(unittest.TestCase): return self.http('/api/bluetooth/action', {'operation': operation, 'address': CAR}, self.cookie if cookie is None else cookie) + def power(self, enabled=False): + return self.http('/api/bluetooth/action', {'operation': 'power', 'enabled': enabled}, self.cookie) + + def test_power_goes_to_the_daemon_holding_the_adapter_and_falls_back_without_it(self): + snapshot = self.bluetooth.snapshot() + snapshot['powered'] = False + self.aa.result = {'handled': True, 'bluetooth': snapshot} + code, result, _ = self.power(False) + self.assertEqual((code, result), (200, snapshot)) + self.assertEqual(self.bluetooth.calls, []) + command, arguments = self.aa.calls[-1] + self.assertEqual((command, arguments['enabled']), ('bluetooth_power', False)) + self.assertEqual(self.http('/api/android-auto/source/' + arguments['source'])[0], 410) + self.aa.result = {'handled': False} + self.assertEqual(self.power(False)[0], 200) + self.assertEqual(self.bluetooth.calls[-1][0], 'power') + self.aa.error = OSError('daemon is not running') + self.assertEqual(self.power(True)[0], 200) + self.assertEqual([call[0] for call in self.bluetooth.calls], ['power', 'power']) + self.params.enabled = False + calls = len(self.aa.calls) + self.assertEqual(self.power(False)[0], 200) + self.assertEqual(len(self.aa.calls), calls, 'Android Auto off: the daemon is never contacted') + + def test_power_refusals_from_the_daemon_never_fall_back(self): + for error, code in [('projection_active', 409), ('busy', 409), ('park_required', 409), ('service_unavailable', 503)]: + self.aa.result = {'handled': True, 'error_code': error} + actual, result, _ = self.power(False) + self.assertEqual((actual, result['code']), (code, error)) + self.assertEqual(self.bluetooth.calls, []) + def test_layout_requires_actual_auth_then_saves_only_projection_document(self): self.assertEqual(self.http('/api/android-auto/layout')[0], 401) payload = self.layout_payload() @@ -170,6 +201,21 @@ class ProjectionBluetoothHttpTest(unittest.TestCase): self.assertEqual(len(self.bluetooth.calls), 2) self.assertEqual(self.bluetooth.calls[-1][0], 'disconnect') + def test_forget_is_delegated_to_a_running_projection_and_falls_back_without_the_daemon(self): + snapshot = self.bluetooth.snapshot() + self.aa.result = {'handled': True, 'bluetooth': snapshot} + code, result, _ = self.action('forget') + self.assertEqual((code, result), (200, snapshot)) + self.assertEqual(self.aa.calls[0][1]['operation'], 'forget') + self.assertEqual(self.bluetooth.calls, []) + self.aa.result = {'handled': False} + self.assertEqual(self.action('forget')[0], 200) + self.assertEqual(self.bluetooth.calls[-1][0], 'forget') + self.aa.error = OSError('daemon is not running') + self.assertEqual(self.action('forget')[0], 200) + self.assertEqual([call[0] for call in self.bluetooth.calls], ['forget', 'forget']) + self.assertEqual(self.action('connect')[0], 503) + def test_owner_busy_and_error_codes_never_fall_back(self): for error, code in [('busy', 409), ('park_required', 409), ('changed', 409), ('service_unavailable', 503)]: self.aa.result = {'handled': True, 'error_code': error} diff --git a/openpilot/starpilot/galaxy/test_projection_phone_window_http.py b/openpilot/starpilot/galaxy/test_projection_phone_window_http.py new file mode 100644 index 0000000000..287114a683 --- /dev/null +++ b/openpilot/starpilot/galaxy/test_projection_phone_window_http.py @@ -0,0 +1,147 @@ +"""Galaxy routes a phone pairing window to Android Auto while it projects; radio and AA IPC are fakes.""" +import http.client +import json +from pathlib import Path +import tempfile +import threading +from types import SimpleNamespace +import unittest +from unittest import mock + +from openpilot.starpilot.galaxy.access import GalaxyAccessOwner +from openpilot.starpilot.galaxy.server import make_server + +PROMPT = '0123456789abcdef0123456789abcdef' +PHONE = '11:22:33:44:55:66' + + +class Params: + def __init__(self, root): self.root, self.enabled = root, True + def get(self, key): return None + def get_param_path(self, key): return str(self.root / key) + def get_bool(self, key): return self.enabled + + +class Bluetooth: + def __init__(self): self.calls = [] + def snapshot(self, **kwargs): + return {'version': 1, 'available': True, 'parked': True, 'powered': True, + 'discovering': False, 'devices': [], 'errorCode': None, 'pairing': None} + def request(self, operation, **kwargs): + self.calls.append(operation) + return self.snapshot() + def cancel_session(self, identity): pass + + +class AA: + def __init__(self): + self.calls, self.window, self.http, self.paired = [], True, None, None + def call(self, command, **kwargs): + assert command == 'phone_window' + self.calls.append(kwargs) + code, data, _ = self.http('/api/android-auto/source/' + kwargs['source']) # ty: ignore[call-non-callable] # set by the test + assert code == 200 and data == {'valid': True}, 'Source proof was not live' + if not self.window: + return {'handled': False} + pairing = {'address': PHONE, 'state': 'pairing', 'incoming': True, 'name': 'iPhone', + 'prompt': {'id': PROMPT, 'kind': 'confirmation', 'value': '123456'}} + return {'handled': True, 'bluetooth': {'version': 1, 'devices': [], 'pairing': pairing, 'windowPaired': self.paired}} + + +class ProjectionPhoneWindowHttpTest(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + claim_path = mock.patch('openpilot.starpilot.galaxy_bluetooth.bridge.claim.CLAIM_PATH', Path(temporary.name) / 'claim.json') + claim_path.start() + self.addCleanup(claim_path.stop) + root = Path(temporary.name) + self.params = Params(root) + self.access = GalaxyAccessOwner(root / 'access') + self.assertTrue(self.access.configure('password123', lambda: True)) + self.bluetooth, self.aa = Bluetooth(), AA() + self.spotify = SimpleNamespace(prepared=[], adopted=[]) + self.spotify.prepare_phone_pairing = lambda: self.spotify.prepared.append(True) + self.spotify.adopt_phone = lambda *args: self.spotify.adopted.append(args) + self.server = make_server(port=0, owner=self.access, parked=lambda: True, + android_auto_setup=SimpleNamespace(enabled=lambda: self.params.enabled), + android_auto_client=self.aa, bluetooth=self.bluetooth, spotify_setup=self.spotify) + self.worker = threading.Thread(target=self.server.serve_forever, kwargs={'poll_interval': .01}, daemon=True) + self.worker.start() + self.addCleanup(self.stop) + self.aa.http = self.http + code, _, headers = self.http('/api/auth/login', {'password': 'password123'}) + self.assertEqual(code, 200) + self.cookie = headers['Set-Cookie'].split(';', 1)[0] + + def stop(self): + self.server.shutdown() + self.worker.join(timeout=2) + self.server.server_close() + + def http(self, path, payload=None, cookie=None): + connection = http.client.HTTPConnection('127.0.0.1', self.server.server_port, timeout=4) + headers = {'Cookie': self.cookie if cookie is None and hasattr(self, 'cookie') else cookie or ''} + if payload is not None: + headers.update({'Content-Type': 'application/json', 'Origin': f'http://127.0.0.1:{self.server.server_port}'}) + try: + connection.request('POST' if payload is not None else 'GET', path, + body=json.dumps(payload) if payload is not None else None, headers=headers) + response = connection.getresponse() + return response.status, json.loads(response.read()), dict(response.getheaders()) + finally: + connection.close() + + def action(self, payload): + return self.http('/api/bluetooth/action', payload) + + def test_window_opens_answers_and_reports_through_android_auto_while_projecting(self): + code, result, _ = self.action({'operation': 'accept_pair'}) + self.assertEqual(code, 200) + self.assertTrue(result['pairing']['incoming']) + self.assertNotIn('windowPaired', result) + self.assertEqual(self.bluetooth.calls, []) + self.assertEqual(self.spotify.prepared, [True]) + code, status, _ = self.http('/api/bluetooth/status') + self.assertEqual((code, status['pairing']['prompt']['value']), (200, '123456')) + self.aa.paired = {'address': PHONE, 'name': 'iPhone', 'uuids': ['0000110a-0000-1000-8000-00805f9b34fb']} + code, _, _ = self.action({'operation': 'pairing_response', 'promptId': PROMPT, 'accepted': True}) + self.assertEqual(code, 200) + self.assertEqual(self.spotify.adopted, [(PHONE, 'iPhone', ('0000110a-0000-1000-8000-00805f9b34fb',))]) + sources = {call['source'] for call in self.aa.calls} + self.assertEqual(len(sources), 1, 'every window request must reuse one source') + self.assertEqual(self.aa.calls[2]['prompt_id'], PROMPT) + self.assertEqual(self.bluetooth.calls, []) + + def test_without_projection_the_comma_owner_opens_the_window(self): + self.aa.window = False + code, _, _ = self.action({'operation': 'accept_pair'}) + self.assertEqual(code, 200) + self.assertEqual(self.bluetooth.calls, ['accept_pair']) + self.aa.calls.clear() + self.http('/api/bluetooth/status') + self.assertEqual(self.aa.calls, [], 'an unhandled window must not keep a source alive') + + def test_android_auto_off_never_contacts_the_daemon(self): + self.params.enabled = False + self.assertEqual(self.action({'operation': 'accept_pair'})[0], 200) + self.assertEqual(self.aa.calls, []) + self.assertEqual(self.bluetooth.calls, ['accept_pair']) + + def test_accessory_scan_pair_prompt_and_cancel_are_delegated(self): + for operation in ('scan', 'stop_scan'): + self.assertEqual(self.action({'operation': operation})[0], 200) + self.assertNotEqual(self.http('/api/android-auto/source/' + self.aa.calls[-1]['source'])[1], {'valid': True}) + self.assertEqual(self.action({'operation': 'pair', 'address': PHONE})[0], 200) + source = self.aa.calls[-1]['source'] + self.assertEqual(self.aa.calls[-1]['address'], PHONE) + self.assertEqual(self.http('/api/bluetooth/status')[0], 200) + self.assertEqual(self.action({'operation': 'pairing_response', 'promptId': PROMPT, 'accepted': True})[0], 200) + self.assertEqual(self.action({'operation': 'cancel_pair'})[0], 200) + self.assertEqual({c['source'] for c in self.aa.calls[-4:]}, {source}) + self.assertEqual(self.bluetooth.calls, []) + self.assertEqual(self.spotify.prepared, []) + + +if __name__ == '__main__': + unittest.main() diff --git a/openpilot/starpilot/galaxy/tests/test_auth_runtime.py b/openpilot/starpilot/galaxy/tests/test_auth_runtime.py index e8965e7896..c8516ba088 100644 --- a/openpilot/starpilot/galaxy/tests/test_auth_runtime.py +++ b/openpilot/starpilot/galaxy/tests/test_auth_runtime.py @@ -272,7 +272,8 @@ class AuthRuntimeTest(unittest.TestCase): held = [] with mock.patch.object(self.server.RequestHandlerClass, 'setup', observed_setup), \ - mock.patch.object(self.server.RequestHandlerClass, 'finish', observed_finish): + mock.patch.object(self.server.RequestHandlerClass, 'finish', observed_finish), \ + mock.patch.object(self.server, 'loopback', return_value=False): try: for _ in range(self.server.MAX_CONNECTIONS): held.append(socket.create_connection(('127.0.0.1', self.server.server_port), timeout=2)) diff --git a/openpilot/starpilot/galaxy/tests/test_bluetooth_http.py b/openpilot/starpilot/galaxy/tests/test_bluetooth_http.py index a3f74743da..7580fed9d5 100644 --- a/openpilot/starpilot/galaxy/tests/test_bluetooth_http.py +++ b/openpilot/starpilot/galaxy/tests/test_bluetooth_http.py @@ -47,6 +47,9 @@ class BluetoothHttpTest(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) + claim_path = mock.patch('openpilot.starpilot.galaxy_bluetooth.bridge.claim.CLAIM_PATH', Path(temporary.name) / 'claim.json') + claim_path.start() + self.addCleanup(claim_path.stop) access = GalaxyAccessOwner(Path(temporary.name) / 'access') self.assertTrue(access.configure('password123', lambda: True)) self.access = access @@ -77,12 +80,68 @@ class BluetoothHttpTest(unittest.TestCase): finally: connection.close() + def test_accept_pair_opens_a_window_and_prepares_the_music_receiver(self): + self.stop() + aa_setup = mock.Mock() + aa_setup.enabled.return_value = False + self.server = make_server(port=0, owner=self.access, bluetooth=self.bluetooth, android_auto_setup=aa_setup, parked=lambda: True) + self.worker = threading.Thread(target=self.server.serve_forever, kwargs={'poll_interval': 0.01}, daemon=True) + self.worker.start() + _, _, headers = self.request('/api/auth/login', {'password': 'password123'}) + cookie = headers['Set-Cookie'].split(';', 1)[0] + with mock.patch('openpilot.starpilot.galaxy.spotify_setup.SpotifySetup.prepare_phone_pairing') as prepare: + self.assertEqual(self.request('/api/bluetooth/action', {'operation': 'accept_pair'}, cookie)[0], 200) + prepare.assert_called_once_with() + self.assertEqual(self.request('/api/bluetooth/action', {'operation': 'accept_pair', 'address': 'AA:BB:CC:DD:EE:FF'}, cookie)[0], 400) + prepare.assert_called_once_with() + operation, address, *_ = self.bluetooth.calls[-1] + self.assertEqual((operation, address), ('accept_pair', None)) + self.assertEqual(len(self.bluetooth.calls), 1) + + def test_the_music_window_also_opens_and_closes_the_app_claim(self): + from openpilot.starpilot.galaxy_bluetooth.bridge import claim + self.stop() + aa_setup = mock.Mock() + aa_setup.enabled.return_value = False + self.server = make_server(port=0, owner=self.access, bluetooth=self.bluetooth, android_auto_setup=aa_setup, parked=lambda: True) + self.worker = threading.Thread(target=self.server.serve_forever, kwargs={'poll_interval': 0.01}, daemon=True) + self.worker.start() + _, _, headers = self.request('/api/auth/login', {'password': 'password123'}) + cookie = headers['Set-Cookie'].split(';', 1)[0] + with tempfile.TemporaryDirectory() as directory, mock.patch.object(claim, 'CLAIM_PATH', Path(directory) / 'claim.json'), \ + mock.patch('openpilot.starpilot.galaxy.spotify_setup.SpotifySetup.prepare_phone_pairing'), \ + mock.patch.object(self.bluetooth, 'snapshot', return_value={'adapterAddress': 'AA:BB:CC:DD:EE:FF', 'adapterName': 'comma'}): + self.assertIsNone(claim.window(claim.CLAIM_PATH)) + self.assertEqual(self.request('/api/bluetooth/action', {'operation': 'accept_pair'}, cookie)[0], 200) + window = claim.window(claim.CLAIM_PATH) + self.assertEqual((window['adapterAddress'], window['adapterName']), ('AA:BB:CC:DD:EE:FF', 'comma')) + self.assertEqual(self.request('/api/bluetooth/action', {'operation': 'cancel_pair'}, cookie)[0], 200) + self.assertIsNone(claim.window(claim.CLAIM_PATH)) + self.assertEqual(self.request('/api/bluetooth/action', {'operation': 'scan'}, cookie)[0], 200) + self.assertIsNone(claim.window(claim.CLAIM_PATH)) + + def test_forgetting_a_device_also_forgets_its_app_key(self): + self.stop() + aa_setup = mock.Mock() + aa_setup.enabled.return_value = False + self.server = make_server(port=0, owner=self.access, bluetooth=self.bluetooth, android_auto_setup=aa_setup, parked=lambda: True) + self.worker = threading.Thread(target=self.server.serve_forever, kwargs={'poll_interval': 0.01}, daemon=True) + self.worker.start() + _, _, headers = self.request('/api/auth/login', {'password': 'password123'}) + cookie = headers['Set-Cookie'].split(';', 1)[0] + with mock.patch('openpilot.starpilot.galaxy_bluetooth.bridge.pairing.remove_by_address') as remove: + self.assertEqual(self.request('/api/bluetooth/action', {'operation': 'connect', 'address': 'AA:BB:CC:DD:EE:FF'}, cookie)[0], 200) + remove.assert_not_called() + self.assertEqual(self.request('/api/bluetooth/action', {'operation': 'forget', 'address': 'AA:BB:CC:DD:EE:FF'}, cookie)[0], 200) + remove.assert_called_once_with('AA:BB:CC:DD:EE:FF') + def test_authenticated_status_and_parked_mutation(self): self.assertEqual(self.request('/api/bluetooth/status')[0], 401) code, _, headers = self.request('/api/auth/login', {'password': 'password123'}) self.assertEqual(code, 200) cookie = headers['Set-Cookie'].split(';', 1)[0] self.assertEqual(self.request('/api/bluetooth/status', cookie=cookie)[1]['powered'], True) + self.assertEqual(self.request('/api/bluetooth/status', cookie=cookie)[1]['claim']['open'], False) self.assertEqual(self.request('/api/bluetooth/action', {'operation': 'connect', 'address': 'AA:BB:CC:DD:EE:FF'}, cookie)[0], 200) self.assertEqual(self.bluetooth.calls[0][:3], ('connect', 'AA:BB:CC:DD:EE:FF', None)) self.bluetooth.parked = False diff --git a/openpilot/starpilot/galaxy/tests/test_companion_http.py b/openpilot/starpilot/galaxy/tests/test_companion_http.py new file mode 100644 index 0000000000..a066286ecb --- /dev/null +++ b/openpilot/starpilot/galaxy/tests/test_companion_http.py @@ -0,0 +1,212 @@ +import http.client +import json +from pathlib import Path +import sys +import threading +from unittest.mock import Mock, patch + +import pytest + +from openpilot.starpilot.galaxy.access import GalaxyAccessOwner +from openpilot.starpilot.galaxy.companion import CompanionRegistry +from openpilot.starpilot.galaxy.server import make_server + + +@pytest.fixture +def companion_http(tmp_path): + registry = CompanionRegistry(tmp_path / 'settings', tmp_path / 'lease', tmp_path / 'status') + access = GalaxyAccessOwner(tmp_path / 'access') + access.configure('password123', lambda: True) + aa = Mock() + aa.enabled.return_value = False + parked = Mock(return_value=True) + server = make_server(port=0, owner=access, companion=registry, parked=parked, android_auto_setup=aa) + server.test_parked = parked + worker = threading.Thread(target=server.serve_forever, kwargs={'poll_interval': .01}, daemon=True) + worker.start() + + def request(path, payload=None, cookie='', origin=None, method=None): + connection = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=3) + headers = {'Cookie': cookie} + if payload is not None: + headers.update({'Content-Type': 'application/json', 'Origin': origin or f'http://127.0.0.1:{server.server_port}'}) + try: + connection.request(method or ('POST' if payload is not None else 'GET'), path, + json.dumps(payload) if payload is not None else None, headers) + response = connection.getresponse() + return response.status, response.read(), dict(response.getheaders()) + finally: + connection.close() + try: + yield request, registry, server + finally: + server.shutdown() + worker.join(timeout=2) + server.server_close() + + +def test_auth_and_local_mutations(companion_http): + request, registry, _ = companion_http + assert request('/api/companion/status')[0] == 401 + assert request('/api/companion/settings', {'enabled': True})[0] == 401 + with patch('openpilot.starpilot.galaxy.server.direct_local_connection', return_value=True): + status, _, headers = request('/api/auth/session') + assert status == 200 + cookie = headers['Set-Cookie'].split(';', 1)[0] + assert request('/api/companion/settings', {'enabled': True}, cookie, 'https://evil.test')[0] == 403 + assert not registry.enabled() + assert request('/api/companion/settings', {'enabled': True}, cookie)[0] == 200 + assert registry.enabled() + assert request('/api/companion/settings', {'enabled': 1}, cookie)[0] == 400 + # A signed-in LAN client still cannot register a SOCKS endpoint outside galaxy0. + payload = {'port': 1080, 'user': 'a' * 32, 'pass': 'b' * 32, 'version': '1.0.0', 'protocol': 1} + assert request('/api/companion/uplink', payload, cookie)[0] == 400 + assert registry.lease() is None + status, body, _ = request('/api/companion/status', cookie=cookie) + assert status == 200 and json.loads(body)['enabled'] + assert b'pass' not in body + assert request('/api/companion/qr.svg', cookie=cookie)[0] == 200 + + +def test_apk_direct_download_without_cookie_and_head(companion_http, tmp_path): + request, _, server = companion_http + apk = tmp_path / 'sample.apk' + apk.write_bytes(b'APK' * 30000) + # Path is captured during server construction; patch its file operation. + from openpilot.starpilot.galaxy.companion import APK_PATH + real_open = type(APK_PATH).open + + def open_apk(path, *args, **kwargs): + return real_open(apk if path == APK_PATH else path, *args, **kwargs) + + with patch('openpilot.starpilot.galaxy.server.direct_local_connection', return_value=True), patch('pathlib.Path.open', open_apk): + code, body, headers = request('/app.apk') + assert code == 200 and body == apk.read_bytes() + assert headers['Content-Type'] == 'application/vnd.android.package-archive' + code, body, headers = request('/api/companion/app.apk', method='HEAD') + assert code == 200 and body == b'' and headers['Content-Length'] == '90000' + server.remote_transport = True + assert request('/app.apk')[0] == 403 + + +def phone_bridge_proxy(port): + # The phone bridge imports its siblings directly, exactly as it runs on comma. + bridge = str(Path(__file__).resolve().parents[2] / 'galaxy_bluetooth/bridge') + if bridge not in sys.path: + sys.path.insert(0, bridge) + from proxy import GalaxyProxy + return GalaxyProxy(port) + + +def test_phone_bridge_against_current_galaxy(companion_http): + _, registry, server = companion_http + proxy = phone_bridge_proxy(server.server_port) + request = {'id': 'test', 'method': 'GET', 'path': '/api/companion/status'} + assert proxy.handle(request)['status'] == 200 + token = proxy.cookie + assert token.startswith('galaxy_session=') + import base64 + request.update(method='POST', path='/api/companion/settings', headers={'Content-Type': 'application/json'}, + body=base64.b64encode(b'{"enabled":true}').decode()) + assert proxy.handle(request)['status'] == 200 + assert registry.enabled() + assert proxy.cookie == token + for path in ('/api/companion/uplink', '/api/companion/app.apk'): + request['path'] = path + assert proxy.handle(request)['status'] == 501 + + +def test_bluetooth_runtime_tracks_upstream_restart_and_blocks_replay(companion_http): + import base64 + request, _, server = companion_http + proxy = phone_bridge_proxy(server.server_port) + for instance in ('before-restart', 'after-restart'): + with patch('openpilot.starpilot.galaxy.server.INSTANCE', instance): + result = proxy.handle({'id': 'runtime', 'method': 'GET', 'path': '/api/companion/runtime'}) + assert result['status'] == 200 + runtime = json.loads(base64.b64decode(result['body'])) + assert runtime == json.loads(request('/data/runtime.json')[1]) + assert runtime['instance'] == instance + for path in ('/api/replay/start', '/api/replay/media?sequence=1'): + assert proxy.handle({'id': 'replay', 'method': 'POST', 'path': path})['status'] == 501 + + +def test_web_manifest_lists_verifiable_files_the_apps_accept(companion_http): + import hashlib + from openpilot.starpilot.galaxy.server import WEB + request, _, _ = companion_http + status, body, _ = request('/api/companion/web-manifest') + assert status == 200 + manifest = json.loads(body) + assert manifest['schema'] == 1 + files = manifest['files'] + paths = [entry['path'] for entry in files] + assert paths == sorted(paths) and len(set(paths)) == len(paths) + assert 'data/runtime.json' not in paths and not any(p.endswith('.sample.json') for p in paths) + for entry in files: + data = (WEB / entry['path']).read_bytes() + assert entry['size'] == len(data) and entry['sha256'] == hashlib.sha256(data).hexdigest() + assert request('/' + entry['path'])[1] == data + # The companion apps reject a download that breaks these rules, so catch a new root file or odd name here. + root_files = {'index.html', 'manifest.webmanifest', 'sentry-push-worker.js'} + root_dirs = ('assets/', 'css/', 'js/', 'vendor/', 'data/') + allowed = set('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-./_') + assert 1 <= len(files) <= 512 and sum(entry['size'] for entry in files) <= 32 * 1024 * 1024 + for entry in files: + assert set(entry['path']) <= allowed and entry['size'] <= 4 * 1024 * 1024 + assert entry['path'] in root_files or entry['path'].startswith(root_dirs), entry['path'] + assert {'index.html', 'js/boot.js', 'js/app.js', 'data/catalog.json'} <= set(paths) + + +def test_phone_location_needs_local_session(tmp_path): + access = GalaxyAccessOwner(tmp_path / 'access') + access.configure('password123', lambda: True) + phone = Mock() + phone.publish.side_effect = lambda payload: {'ok': True} if payload.get('ageMs', 0) <= 2000 else (_ for _ in ()).throw(ValueError('Location is too old')) + aa = Mock() + aa.enabled.return_value = False + server = make_server(port=0, owner=access, companion=CompanionRegistry(tmp_path / 's', tmp_path / 'l', tmp_path / 't'), + parked=Mock(return_value=True), android_auto_setup=aa, phone_location=phone) + worker = threading.Thread(target=server.serve_forever, kwargs={'poll_interval': .01}, daemon=True) + worker.start() + origin = f'http://127.0.0.1:{server.server_port}' + + def post(payload, cookie=''): + connection = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=3) + try: + connection.request('POST', '/api/companion/location', json.dumps(payload), + {'Cookie': cookie, 'Content-Type': 'application/json', 'Origin': origin}) + response = connection.getresponse() + return response.status, json.loads(response.read()) + finally: + connection.close() + try: + fix = {'latitude': 40., 'longitude': -110., 'horizontalAccuracy': 5., 'ageMs': 200} + assert post(fix)[0] == 401 + with patch('openpilot.starpilot.galaxy.server.direct_local_connection', return_value=True): + connection = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=3) + connection.request('GET', '/api/auth/session') + cookie = connection.getresponse().getheader('Set-Cookie').split(';', 1)[0] + connection.close() + assert post(fix, cookie) == (200, {'ok': True}) + assert post({**fix, 'ageMs': 5000}, cookie) == (400, {'error': 'Location is too old'}) + server.remote_transport = True + assert post(fix, cookie)[0] == 403 + assert phone.publish.call_count == 2 + finally: + server.shutdown() + worker.join(timeout=2) + server.server_close() + + +def test_bluetooth_ready_needs_a_session_and_reports_what_blocks_pairing(companion_http): + request, _, _ = companion_http + assert request('/api/companion/bluetooth-ready')[0] == 401 + with patch('openpilot.starpilot.galaxy.server.direct_local_connection', return_value=True): + cookie = request('/api/auth/session')[2]['Set-Cookie'].split(';', 1)[0] + status, body, _ = request('/api/companion/bluetooth-ready', cookie=cookie) + assert status == 200 + answer = json.loads(body) + assert answer['schema'] == 1 and isinstance(answer['ready'], bool) + assert answer['code'] in {'ready', 'starting', 'park_required', 'bluetooth_off', 'bluetooth_unavailable', 'bridge_starting'} + assert answer['message'] diff --git a/openpilot/starpilot/galaxy/tests/test_http_server.py b/openpilot/starpilot/galaxy/tests/test_http_server.py index 4e91daf91f..77d99c1014 100644 --- a/openpilot/starpilot/galaxy/tests/test_http_server.py +++ b/openpilot/starpilot/galaxy/tests/test_http_server.py @@ -73,6 +73,9 @@ def server(tmp_path, monkeypatch): self.json(413, {'error': 'Rejected before reading the body'}) with _LocalHTTPServer(('127.0.0.1', 0), Handler) as http: + services: set[int] = set() + http.loopback = lambda request: request.getpeername()[1] in services + http.services = services thread = threading.Thread(target=http.serve_forever, kwargs={'poll_interval': .01}, daemon=True) thread.start() try: @@ -158,3 +161,46 @@ def test_idle_persistent_connections_release_capacity(server): finally: for connection in held: connection.close() + + +def test_full_browser_pool_still_serves_comma_services(server): + running, _, _ = server + held = [] + try: + for _ in range(running.MAX_CONNECTIONS + 2): + held.append(socket.create_connection(('127.0.0.1', running.server_port), timeout=2)) + service = socket.socket() + service.bind(('127.0.0.1', 0)) + running.services.add(service.getsockname()[1]) + service.settimeout(1) + service.connect(('127.0.0.1', running.server_port)) + with closing(service): + service.sendall(b'GET /service HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n') + assert service.recv(64).startswith(b'HTTP/1.1 200') + finally: + for connection in held: + connection.close() + + +def test_drain_turns_away_requests_still_waiting_for_capacity(server): + running, state, _ = server + held = [socket.create_connection(('127.0.0.1', running.server_port), timeout=2) for _ in range(running.MAX_CONNECTIONS)] + try: + with closing(socket.create_connection(('127.0.0.1', running.server_port), timeout=2)) as waiting: + waiting.sendall(b'GET /late HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n') + drained: list[bool] = [] + drain = threading.Thread(target=lambda: drained.append(running.drain_requests(2))) + drain.start() + for connection in held: + connection.close() + held.clear() + drain.join(3) + assert drained == [True] + try: + assert waiting.recv(64) == b'' + except ConnectionResetError: + pass + assert '/late' not in state['requests'] + finally: + for connection in held: + connection.close() diff --git a/openpilot/starpilot/galaxy/tests/test_pairing_ready.py b/openpilot/starpilot/galaxy/tests/test_pairing_ready.py new file mode 100644 index 0000000000..249a4ed3bc --- /dev/null +++ b/openpilot/starpilot/galaxy/tests/test_pairing_ready.py @@ -0,0 +1,40 @@ +from unittest.mock import patch + +from openpilot.starpilot.galaxy.pairing_ready import BOOT_GRACE_SECONDS, TRANSIENT, bridge_state, pairing_readiness + +GOOD = {'available': True, 'powered': True, 'errorCode': None} + + +def ask(*, uptime=600., parked=True, state=None, bridge=('ready', 'Ready to pair')): + def snapshot(): + if isinstance(state, Exception): + raise state + return GOOD if state is None else state + return pairing_readiness(parked=lambda: parked, snapshot=snapshot, bridge=lambda: bridge, uptime=lambda: uptime) + + +def test_ready_only_when_everything_a_phone_needs_is_up(): + assert ask() == {'schema': 1, 'ready': True, 'code': 'ready', 'message': 'Ready to pair.'} + + +def test_each_blocker_has_its_own_code_in_the_order_a_person_would_fix_them(): + assert ask(uptime=BOOT_GRACE_SECONDS - 1)['code'] == 'starting' + assert ask(parked=False)['code'] == 'park_required' + assert ask(state={'available': True, 'powered': False})['code'] == 'bluetooth_off' + assert ask(state={'available': True, 'powered': True, 'errorCode': 'adapter_unavailable'})['code'] == 'bluetooth_unavailable' + assert ask(state=OSError('gone'))['code'] == 'bluetooth_unavailable' + assert ask(state=ValueError('anything else'))['code'] == 'bluetooth_unavailable', 'never an error' + assert ask(bridge=('pending', 'Bridge startup pending')) == { + 'schema': 1, 'ready': False, 'code': 'bridge_starting', 'message': 'Bridge startup pending'} + assert ask(uptime=1, parked=False)['code'] == 'starting', 'boot comes first' + + +def test_only_boot_and_bridge_start_are_worth_waiting_for(): + assert TRANSIENT == {'starting', 'bridge_starting'} + assert ask(parked=False)['code'] not in TRANSIENT + assert ask(state={'available': True, 'powered': False})['code'] not in TRANSIENT + + +def test_an_unreadable_or_stubbed_bridge_module_reads_as_not_ready_instead_of_failing(): + with patch.dict('sys.modules', {'openpilot.starpilot.galaxy_bluetooth.bootstrap': object()}): + assert bridge_state() == ('pending', 'Bridge startup pending') diff --git a/openpilot/starpilot/galaxy/tests/test_preview_contract.py b/openpilot/starpilot/galaxy/tests/test_preview_contract.py index 4ce82f7c41..3dfcba5009 100644 --- a/openpilot/starpilot/galaxy/tests/test_preview_contract.py +++ b/openpilot/starpilot/galaxy/tests/test_preview_contract.py @@ -18,8 +18,7 @@ class PreviewContractTest(unittest.TestCase): self.assertTrue(all(path.startswith("/") and path != "/tools" for path in paths)) self.assertEqual([tool["path"] for tool in catalog["tools"] if tool["availability"] == "partial-preview"], ["/logs"]) self.assertEqual([tool["path"] for tool in catalog["tools"] if tool["availability"] == "local-only"], - ["/bluetooth", "/cameras", "/galaxy", "/manage_models", "/model_laboratory", - "/navigation", "/system", "/theme_maker", "/tuning/plots", "/vehicle", "/android-auto"]) + ["/bluetooth", "/cameras", "/galaxy", "/manage_models", "/model_laboratory", "/navigation", "/system", "/theme_maker", "/tuning/plots", "/vehicle", "/android-auto", "/spotify"]) self.assertEqual([tool["path"] for tool in catalog["tools"] if tool["availability"] == "always"], []) self.assertTrue(all(tool["availability"] in {"always", "unavailable", "partial-preview", "local-only"} for tool in catalog["tools"])) self.assertTrue(all(tool["name"] and tool["description"] and tool["icon"] for tool in catalog["tools"])) diff --git a/openpilot/starpilot/galaxy/tests/test_remote_pairing.py b/openpilot/starpilot/galaxy/tests/test_remote_pairing.py index 329e99bd65..8d27e5c6c4 100644 --- a/openpilot/starpilot/galaxy/tests/test_remote_pairing.py +++ b/openpilot/starpilot/galaxy/tests/test_remote_pairing.py @@ -19,6 +19,9 @@ class RemotePairingTest(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) + claim_path = mock.patch('openpilot.starpilot.galaxy_bluetooth.bridge.claim.CLAIM_PATH', Path(temporary.name) / 'claim.json') + claim_path.start() + self.addCleanup(claim_path.stop) root = Path(temporary.name) self.owner = GalaxyAccessOwner(root / 'credentials') self.pairing = RemotePairing(root / 'pairing') @@ -186,7 +189,7 @@ class RemotePairingTest(unittest.TestCase): wrong_cookie = 'galaxy_session=' + record['slug'] + '%3A' + 'a' * 64 self.assertEqual(self.request(self.remote, '/api/galaxy/device-name', host=host, cookie=wrong_cookie)[0], 401) created = [] - def bluetooth_factory(authority, session_valid): + def bluetooth_factory(authority, session_valid, **_kwargs): owner = mock.Mock() owner.session_valid = session_valid owner.identities = [] @@ -200,7 +203,7 @@ class RemotePairingTest(unittest.TestCase): mock.patch('openpilot.starpilot.galaxy.settings.LiveContextSource'): status, body, _ = self.request(self.remote, '/api/bluetooth/status', host=host, cookie=cookie) self.assertEqual(status, 200) - self.assertEqual(json.loads(body), {'available': True}) + self.assertIs(json.loads(body)['available'], True) bluetooth = created[0] identity = bluetooth.identities[0] self.assertTrue(bluetooth.session_valid(identity)) diff --git a/openpilot/starpilot/galaxy/tests/test_spotify_setup.py b/openpilot/starpilot/galaxy/tests/test_spotify_setup.py new file mode 100644 index 0000000000..9ec42298a1 --- /dev/null +++ b/openpilot/starpilot/galaxy/tests/test_spotify_setup.py @@ -0,0 +1,251 @@ +import http.client +import io +import json +import threading + +import pytest + +from openpilot.starpilot.galaxy.access import GalaxyAccessOwner +from openpilot.starpilot.galaxy.http_server import install_origin_allowed +from openpilot.starpilot.galaxy.server import make_server +from openpilot.starpilot.galaxy.spotify_setup import SetupRejected, SpotifySetup +from openpilot.starpilot.soloist import credentials, preferences +from openpilot.starpilot.soloist.paths import Layout + +SESSION = ('token', 1) +DEVICES = [{'address': 'AA:BB:CC:DD:EE:FF', 'name': 'My Car', 'paired': True, 'connected': True}, + {'address': '11:22:33:44:55:66', 'name': 'Nearby', 'paired': False, 'connected': False}] + + +class FakeImport: + def __init__(self, layout): + self.work_dir = layout.imports + self.started = [] + self.running = False + def busy(self): return self.running + def status(self): return {'state': 'running' if self.running else 'idle'} + def start(self, *, path, enabled, may_activate): + self.started.append(path.read_bytes()) + self.running = True + + +class FakeClient: + def __init__(self): + self.available = False + self.calls = [] + def call(self, command, **payload): + if not self.available: + raise RuntimeError('Spotify isn’t running') + self.calls.append((command, payload)) + if command == 'status': + return {'status': {'state': 'ready', 'player': None}} + return {'ok': True} + + +@pytest.fixture +def owner(tmp_path): + state = {'parked': True, 'valid': True} + layout = Layout(tmp_path / 'soloist') + client = FakeClient() + setup = SpotifySetup(parked=lambda: state['parked'], session_valid=lambda _s: state['valid'], layout=layout, + client=client, import_job=FakeImport(layout), # ty: ignore[invalid-argument-type] # test doubles + paired_devices=lambda _s: DEVICES) + return setup, state, layout, client + + +def test_status_never_contains_the_key(owner): + setup, _, layout, _ = owner + setup.set_key(SESSION, 'sk_live_0123456789abcdef') + status = setup.status(SESSION) + assert status['keyPresent'] is True and 'sk_live' not in json.dumps(status) + assert status['devices'] == [{'address': 'AA:BB:CC:DD:EE:FF', 'name': 'My Car', 'connected': True}] + assert status['links']['download'].startswith('https://developer.spotify.com/') + with pytest.raises(SetupRejected): + setup.set_key(SESSION, 'bad key') + + +def test_output_must_be_a_paired_device_and_needs_park(owner): + setup, state, layout, _ = owner + with pytest.raises(SetupRejected): + setup.save_preferences(SESSION, {'output': 'bluetooth', 'outputAddress': '11:22:33:44:55:66'}) + setup.save_preferences(SESSION, {'output': 'bluetooth', 'outputAddress': 'aa:bb:cc:dd:ee:ff'}) + assert preferences.load(layout).output_name == 'My Car' + state['parked'] = False + with pytest.raises(SetupRejected): + setup.save_preferences(SESSION, {'output': 'test', 'outputAddress': ''}) + setup.save_preferences(SESSION, {'deviceName': 'Civic'}) # naming is not configuration of the player output + assert preferences.load(layout).device_name == 'Civic' + with pytest.raises(SetupRejected): + setup.save_preferences(SESSION, {'apiKey': 'x'}) + + +def test_upload_streams_to_the_import_job_only_when_parked(owner): + setup, state, _, _ = owner + setup.upload(SESSION, io.BytesIO(b'\x1f\x8bdata'), 6) + assert setup.job.started == [b'\x1f\x8bdata'] + setup.job.running = False + state['parked'] = False + with pytest.raises(SetupRejected): + setup.upload(SESSION, io.BytesIO(b'x'), 1) + state['parked'], state['valid'] = True, False + with pytest.raises(SetupRejected): + setup.upload(SESSION, io.BytesIO(b'x'), 1) + + +def test_remove_deletes_only_feature_data(owner, monkeypatch): + setup, _, layout, _ = owner + credentials.store('sk_live_0123456789abcdef', layout) + preferences.set_enabled(True, layout) + monkeypatch.setattr('openpilot.starpilot.soloist.supervisor.reap_orphans', lambda _l: 0) + status = setup.remove(SESSION) + assert not layout.root.exists() and status['enabled'] is False and status['keyPresent'] is False + + +def test_controls_go_through_the_supervisor(owner): + setup, _, _, client = owner + client.available = True + setup.control(SESSION, 'pause', {}) + assert client.calls[-1] == ('control', {'action': 'pause', 'args': {}}) + with pytest.raises(SetupRejected): + setup.control(SESSION, 'pause', []) + + +@pytest.fixture +def api(tmp_path, owner): + setup, state, layout, client = owner + access = GalaxyAccessOwner(tmp_path / 'access') + assert access.configure('password123', lambda: True) + server = make_server(port=0, owner=access, parked=lambda: state['parked'], spotify_setup=setup) + worker = threading.Thread(target=server.serve_forever, kwargs={'poll_interval': 0.01}, daemon=True) + worker.start() + def request(method, path, body=None, *, cookie='', content_type='application/json'): + conn = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=3) + headers = {'Cookie': cookie} + if method != 'GET': + headers['Origin'] = f'http://127.0.0.1:{server.server_port}' + if body is not None: + headers['Content-Type'] = content_type + try: + conn.request(method, path, body=body, headers=headers) + response = conn.getresponse() + return response.status, json.loads(response.read()) + finally: + conn.close() + conn = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=3) + conn.request('POST', '/api/auth/login', body=json.dumps({'password': 'password123'}), + headers={'Content-Type': 'application/json', 'Origin': f'http://127.0.0.1:{server.server_port}'}) + response = conn.getresponse() + response.read() + cookie = response.getheader('Set-Cookie').split(';', 1)[0] + conn.close() + yield request, cookie, setup, layout + server.shutdown() + worker.join(timeout=2) + server.server_close() + + +def test_routes_require_a_session(api): + request, _, _, _ = api + assert request('GET', '/api/spotify/setup')[0] == 401 + assert request('POST', '/api/spotify/key', json.dumps({'key': 'sk_live_0123456789abcdef'}))[0] == 401 + assert request('POST', '/api/spotify/upload', b'\x1f\x8b', content_type='application/octet-stream')[0] == 401 + assert request('DELETE', '/api/spotify')[0] == 401 + + +def test_routes_round_trip(api, monkeypatch): + request, cookie, setup, layout = api + code, status = request('GET', '/api/spotify/setup', cookie=cookie) + assert code == 200 and status['release'] == {'installed': False} + code, status = request('POST', '/api/spotify/key', json.dumps({'key': 'sk_live_0123456789abcdef'}), cookie=cookie) + assert code == 200 and status['keyPresent'] is True and 'sk_live' not in json.dumps(status) + assert request('POST', '/api/spotify/key', json.dumps({'key': 'nope'}), cookie=cookie)[0] == 409 + assert request('POST', '/api/spotify/key', json.dumps({'key': 'sk_live_0123456789abcdef', 'x': 1}), cookie=cookie)[0] == 400 + code, status = request('POST', '/api/spotify/enable', json.dumps({'enabled': True}), cookie=cookie) + assert code == 200 and status['enabled'] is True and layout.enabled_flag.exists() + code, status = request('POST', '/api/spotify/preferences', json.dumps({'output': 'test', 'outputAddress': ''}), cookie=cookie) + assert code == 200 and status['preferences']['output'] == 'test' + # The phone Bluetooth bridge reaches the server from loopback, so installs are refused here. + code, refusal = request('POST', '/api/spotify/upload', b'\x1f\x8bxx', cookie=cookie, content_type='application/octet-stream') + assert code == 403 and 'local network' in refusal['error'] and setup.job.started == [] + assert request('GET', '/api/spotify/setup', cookie=cookie)[1]['uploadAllowed'] is False + # LAN, hotspot and tunnel peers are covered by the policy test below; here only the route's own checks are exercised. + monkeypatch.setattr('openpilot.starpilot.galaxy.server.install_origin_allowed', lambda *_args, **_kwargs: True) + assert request('GET', '/api/spotify/setup', cookie=cookie)[1]['uploadAllowed'] is True + assert request('POST', '/api/spotify/upload', b'\x1f\x8bxx', cookie=cookie, content_type='application/json')[0] == 415 + code, status = request('POST', '/api/spotify/upload', b'\x1f\x8bxx', cookie=cookie, content_type='application/octet-stream') + assert code == 202 and setup.job.started == [b'\x1f\x8bxx'] + assert request('POST', '/api/spotify/control', json.dumps({'action': 'pause'}), cookie=cookie)[0] == 409 # not running + + +def test_phone_source_requires_park_and_a_paired_device_without_spotify(owner): + setup, state, layout, _ = owner + state['parked'] = False + with pytest.raises(SetupRejected, match='Park'): + setup.save_preferences(SESSION, {'source': 'phone'}) + state['parked'] = True + with pytest.raises(SetupRejected, match='Pair your phone'): + setup.save_preferences(SESSION, {'source': 'phone', 'phoneAddress': '11:22:33:44:55:66'}) + result = setup.save_preferences(SESSION, {'source': 'phone', 'phoneAddress': 'AA:BB:CC:DD:EE:FF', 'output': 'android_auto'}) + assert result['preferences']['source'] == 'phone' and result['preferences']['phoneName'] == 'My Car' + assert not result['keyPresent'] and not result['release']['installed'] + with pytest.raises(SetupRejected, match='different device'): + setup.save_preferences(SESSION, {'output': 'bluetooth', 'outputAddress': 'AA:BB:CC:DD:EE:FF'}) + assert preferences.load(layout).output == 'android_auto' + + +@pytest.mark.parametrize('peer,destination,headers,remote,allowed', [ + ('192.168.1.20', '192.168.1.5', {}, False, True), # home LAN + ('172.31.254.10', '172.31.254.1', {}, False, True), # Galaxy hotspot + ('10.0.0.8', '10.0.0.2', {}, False, True), + ('127.0.0.1', '127.0.0.1', {}, False, False), # phone Bluetooth bridge and anything local to the comma + ('127.0.0.1', '127.0.0.1', {}, True, True), # Galaxy tunnel + ('203.0.113.9', '192.168.1.5', {}, False, False), # public peer + ('192.168.1.20', '192.168.1.5', {'X-Forwarded-For': '8.8.8.8'}, False, False), + ('not-an-address', '192.168.1.5', {}, False, False), +]) +def test_install_origin_policy(peer, destination, headers, remote, allowed): + assert install_origin_allowed(peer, destination, headers, remote) is allowed + + +def test_adopt_phone_offers_only_an_unchosen_audio_source_and_never_overrides(owner): + setup, _, layout, _ = owner + a2dp = '0000110a-0000-1000-8000-00805f9b34fb' + assert not setup.adopt_phone('11:22:33:44:55:66', 'Pixel', ['0000111e-0000-1000-8000-00805f9b34fb']) + assert preferences.load(layout).phone_address == '' + assert setup.adopt_phone('11:22:33:44:55:66', 'Pixel', [a2dp.upper()]) + chosen = preferences.load(layout) + assert (chosen.phone_address, chosen.phone_name, chosen.source) == ('11:22:33:44:55:66', 'Pixel', 'phone') + assert not setup.adopt_phone('66:55:44:33:22:11', 'Other', [a2dp]) + assert preferences.load(layout).phone_address == '11:22:33:44:55:66' + + +def test_adopt_phone_keeps_spotify_when_soloist_is_installed(owner): + setup, _, layout, _ = owner + release = layout.current + release.mkdir(parents=True) + (release / 'soloist').write_text('x') + (release / 'release.json').write_text('{}') + assert setup.adopt_phone('11:22:33:44:55:66', 'Pixel', ['0000110a-0000-1000-8000-00805f9b34fb']) + chosen = preferences.load(layout) + assert (chosen.phone_address, chosen.source) == ('11:22:33:44:55:66', 'spotify') + + +def test_prepare_phone_pairing_only_talks_to_a_running_enabled_feature(owner): + setup, _, layout, client = owner + setup.prepare_phone_pairing() + assert client.calls == [] + preferences.set_enabled(True, layout) + setup.prepare_phone_pairing() + assert client.calls == [] + client.available = True + setup.prepare_phone_pairing() + assert client.calls == [('prepare_phone_pairing', {})] + + +def test_unpaired_connected_device_is_not_offered_or_accepted_as_a_phone(owner): + setup, _, _, _ = owner + connected_only = {'address': '66:55:44:33:22:11', 'name': 'Companion phone', 'paired': False, 'connected': True} + setup.paired_devices = lambda _s: [*DEVICES, connected_only] + assert '66:55:44:33:22:11' not in {device['address'] for device in setup.status(SESSION)['devices']} + with pytest.raises(SetupRejected, match='Pair your phone'): + setup.save_preferences(SESSION, {'source': 'phone', 'phoneAddress': '66:55:44:33:22:11'}) diff --git a/openpilot/starpilot/galaxy/web_manifest.py b/openpilot/starpilot/galaxy/web_manifest.py new file mode 100644 index 0000000000..ff189f2b08 --- /dev/null +++ b/openpilot/starpilot/galaxy/web_manifest.py @@ -0,0 +1,39 @@ +"""Inventory of the Galaxy web files, so companion apps can download and verify them from comma.""" +import hashlib +from pathlib import Path + +# Served dynamically or preview-only; never part of the files an app caches. +EXCLUDED = ('data/runtime.json',) + +_cache: dict[str, tuple[tuple, dict]] = {} + + +def _commit() -> str | None: + try: + from openpilot.common.params import Params + value = Params().get('GitCommit') + except Exception: + return None + text = value.decode() if isinstance(value, bytes) else value + return text.strip() or None if isinstance(text, str) else None + + +def _wanted(relative: str) -> bool: + return relative not in EXCLUDED and not relative.endswith('.sample.json') + + +def web_manifest(web: Path) -> dict: + files = [] + for path in sorted(p for p in web.rglob('*') if p.is_file()): + relative = path.relative_to(web).as_posix() + if not _wanted(relative): + continue + stat = path.stat() + identity = (stat.st_ino, stat.st_mtime_ns, stat.st_size) + cached = _cache.get(str(path)) + if cached is None or cached[0] != identity: + body = path.read_bytes() + cached = (identity, {'path': relative, 'size': len(body), 'sha256': hashlib.sha256(body).hexdigest()}) + _cache[str(path)] = cached + files.append(cached[1]) + return {'schema': 1, 'commit': _commit(), 'files': files} diff --git a/openpilot/starpilot/galaxy_bluetooth/bootstrap.py b/openpilot/starpilot/galaxy_bluetooth/bootstrap.py index 274b6c5268..7439dcc4e2 100644 --- a/openpilot/starpilot/galaxy_bluetooth/bootstrap.py +++ b/openpilot/starpilot/galaxy_bluetooth/bootstrap.py @@ -29,15 +29,20 @@ def write_json(path, value): os.unlink(temporary) -def setup_status(data=DATA): +def setup_state(data=DATA): + """(state, message); the state is 'ready' only while a fresh status file says the bridge is ready.""" try: value = json.loads((data / 'setup-status.json').read_text()) # Monotonic timestamps: a file left from a previous boot reads as stale. if not 0 <= time.monotonic() - value['updated'] <= STATUS_STALE: - return 'Bridge startup pending' - return value['message'] + return 'pending', 'Bridge startup pending' + return str(value.get('state', 'pending')), value['message'] except (OSError, ValueError, KeyError, TypeError): - return 'Bridge startup pending' + return 'pending', 'Bridge startup pending' + + +def setup_status(data=DATA): + return setup_state(data)[1] def retry_setup(data=DATA, *, restart=True): diff --git a/openpilot/starpilot/galaxy_bluetooth/bridge/server.py b/openpilot/starpilot/galaxy_bluetooth/bridge/server.py index 6e7b8863de..27da001c7a 100755 --- a/openpilot/starpilot/galaxy_bluetooth/bridge/server.py +++ b/openpilot/starpilot/galaxy_bluetooth/bridge/server.py @@ -53,6 +53,7 @@ class Session: self.closed = False self.phone = None # (id, key) of the phone whose key opened this session self.introduced = False + self.claiming = False def close(self): self.closed = True @@ -86,6 +87,8 @@ class Gateway: if keys != self.keys: # Only sessions of forgotten (or re-keyed) phones are revoked; other phones stay connected. for path, state in list(self.sessions.items()): + if state.phone is None and state.claiming: + continue if state.phone is None or keys.get(state.phone[0]) != state.phone[1]: state.close() del self.sessions[path] @@ -104,7 +107,12 @@ class Gateway: window = claim.window(self.claim_path) if window is None: raise ValueError("Pairing window closed") - phone_id, key = await asyncio.to_thread(add_phone, name, self.key_path) + state.claiming = True + try: + phone_id, key = await asyncio.to_thread(add_phone, name, self.key_path) + state.phone = (phone_id, key) + finally: + state.claiming = False try: body = claim.reply(public, state.challenge, {"key": key.hex(), "id": phone_id, "adapterAddress": window["adapterAddress"], "adapterName": window["adapterName"]}) @@ -116,8 +124,10 @@ class Gateway: linked = await asyncio.to_thread(claim.take_link, self.claim_path) if linked: await asyncio.to_thread(link_address, linked[0], linked[1], self.key_path) - # The session now belongs to the new phone, so the key change does not close it before the reply is read. - state.phone = (phone_id, key) + if state.closed or load_keys(self.key_path).get(phone_id) != key: + state.closed = True + state.phone = None + raise ValueError("Pairing key was removed before delivery") frame = struct.pack(">I", len(body)) + body state.notification_stream = False state.notification_tag = b"" diff --git a/openpilot/starpilot/galaxy_bluetooth/phone_ui.py b/openpilot/starpilot/galaxy_bluetooth/phone_ui.py index fe1fa3b270..98238f7e6d 100644 --- a/openpilot/starpilot/galaxy_bluetooth/phone_ui.py +++ b/openpilot/starpilot/galaxy_bluetooth/phone_ui.py @@ -1,5 +1,6 @@ """One Pair phone button beside the Bluetooth controls: the StarPilot app and phone music pair in one window.""" import base64 +import datetime import json import threading import time @@ -41,6 +42,40 @@ def forget_phone_keys(allowed, address): pass +def app_pairing_title(phone, phones): + """The phone's name; the added date only tells apart phones that share a name.""" + label = phone['label'] + if sum(1 for other in phones if other['label'] == label) > 1: + try: + added = datetime.datetime.fromisoformat(phone['added']) + return f'{label} ({added:%b} {added.day})' + except (KeyError, ValueError): + pass + return label + + +def app_pairing_subtitle(phone): + try: + used = datetime.date.fromisoformat(phone['last_used']) + except (KeyError, ValueError): + return 'not used yet' + return f'last used {used:%b} {used.day}' + + +def forget_app_phone(allowed, phone_id): + if not permitted(allowed): + return + try: + remove_phone(phone_id) + except (OSError, ValueError): + pass + + +def is_app_link(device): + """A phone that is connected to comma's Bluetooth bridge for the StarPilot app but never bonded for music or audio.""" + return bool(device.get('connected')) and not device.get('paired') and not device.get('trusted') + + def forget_app_only(allowed): if not permitted(allowed): return @@ -285,10 +320,10 @@ def large_phone_rows(allowed): rows = [button_item('Pair phone', 'Pair', description='Pairs the StarPilot app and phone music in one step', callback=lambda: open_pairing(allowed), enabled=lambda: permitted(allowed))] if app_only_count(): - rows.append(button_item('Forget app-only phones', 'Forget', - description=f'{app_only_count()} phones have an app key but no Bluetooth device', + rows.append(button_item('Forget app pairings', 'Forget', + description=f'{app_only_count()} saved for the StarPilot app only (no Bluetooth music pairing)', callback=lambda: gui_app.push_widget(ConfirmDialog( - 'Forget phones that only have the StarPilot app? They must pair again.', 'Forget', + 'Forget phones saved for the StarPilot app only? They must pair again.', 'Forget', callback=lambda result: forget_app_only(allowed) if result == DialogResult.CONFIRM else None)), enabled=lambda: permitted(allowed))) return rows diff --git a/openpilot/starpilot/galaxy_bluetooth/tests/integration_secretgood_http.py b/openpilot/starpilot/galaxy_bluetooth/tests/integration_secretgood_http.py new file mode 100644 index 0000000000..8213def919 --- /dev/null +++ b/openpilot/starpilot/galaxy_bluetooth/tests/integration_secretgood_http.py @@ -0,0 +1,91 @@ +"""Run on a SecretGood runtime: real settings preview/confirm through BLE proxy. + +All settings and access credentials are in a temporary directory. No device +Params, vehicle behavior, radio or hardware connections are changed. +""" +import base64 +import json +from pathlib import Path +import sys +import tempfile +import threading +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'bridge')) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'vendor')) +from proxy import GalaxyProxy +from openpilot.common.params import Params +from openpilot.starpilot.galaxy.access import GalaxyAccessOwner +from openpilot.starpilot.galaxy.server import make_server +from openpilot.starpilot.galaxy.settings import AuthorityContext, SettingsGateway + + +class Context: + def __init__(self): self.value = AuthorityContext(True, None, None) + def sample(self): return self.value + + +class SecretGoodHTTPTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.params = Params(str(Path(self.temp.name) / 'params')) + self.context = Context() + self.settings = SettingsGateway(self.params, self.context) + self.access = GalaxyAccessOwner(Path(self.temp.name) / 'access') + self.server = make_server(port=0, owner=self.access, settings=self.settings) + self.worker = threading.Thread(target=self.server.serve_forever, kwargs={'poll_interval': .01}, daemon=True) + self.worker.start() + self.proxy = GalaxyProxy(self.server.server_port) + self.addCleanup(self.stop) + + def stop(self): + self.server.shutdown() + self.worker.join(timeout=2) + self.server.server_close() + + def request(self, path, payload=None, proxy=None): + response = (proxy or self.proxy).handle({'id': 'test', 'path': path, 'method': 'GET' if payload is None else 'POST', + 'body': '' if payload is None else base64.b64encode(json.dumps(payload).encode()).decode()}) + return response['status'], json.loads(base64.b64decode(response['body'])) + + def test_native_ui_and_runtime_identity(self): + status, runtime = self.request('/data/runtime.json') + self.assertEqual(status, 200) + self.assertEqual(runtime['monitor'], 'local') + self.assertEqual(len(runtime['instance']), 16) + status, hub = self.request('/api/settings/pages/hub') + self.assertEqual(status, 200) + self.assertTrue(hub['rows']) + self.assertTrue(any(row.get('page') for row in hub['rows'])) + + def test_display_preference_round_trip_and_cookie_isolation(self): + status, page = self.request('/api/settings/pages/display') + self.assertEqual(status, 200) + index = next(i for i, row in enumerate(page['rows']) if row['label'] == 'Custom Display Settings') + status, preview = self.request('/api/settings/preview', {'view': page['view'], 'row': index, 'direction': 1}) + self.assertEqual(status, 200) + self.assertFalse(self.params.get_bool('StarPilotDisplayPreferencesEnabled')) + other = self.proxy.new_session() + status, _ = self.request('/api/settings/confirm', {'intent': preview['intent'], 'confirmed': True}, proxy=other) + self.assertNotEqual(status, 200) + status, result = self.request('/api/settings/confirm', {'intent': preview['intent'], 'confirmed': True}) + self.assertEqual((status, result), (200, {'saved': True})) + self.assertTrue(self.params.get_bool('StarPilotDisplayPreferencesEnabled')) + self.assertNotEqual(self.proxy.cookie, other.cookie) + + def test_fresh_handshake_rejects_old_preview(self): + status, page = self.request('/api/settings/pages/display') + self.assertEqual(status, 200) + index = next(i for i, row in enumerate(page['rows']) if row['label'] == 'Custom Display Settings') + status, preview = self.request('/api/settings/preview', {'view': page['view'], 'row': index, 'direction': 1}) + self.assertEqual(status, 200) + # A fresh encrypted handshake must not reuse an old page's mutation intent. + other = self.proxy.new_session() + status, _ = self.request('/api/settings/confirm', {'intent': preview['intent'], 'confirmed': True}, proxy=other) + self.assertNotEqual(status, 200) + self.assertFalse(self.params.get_bool('StarPilotDisplayPreferencesEnabled')) + + +if __name__ == '__main__': + unittest.main() diff --git a/openpilot/starpilot/galaxy_bluetooth/tests/test_claim.py b/openpilot/starpilot/galaxy_bluetooth/tests/test_claim.py index 7b6e22fc6d..861f669faf 100644 --- a/openpilot/starpilot/galaxy_bluetooth/tests/test_claim.py +++ b/openpilot/starpilot/galaxy_bluetooth/tests/test_claim.py @@ -7,6 +7,7 @@ import sys import tempfile import time import unittest +from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "bridge")) sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "vendor")) @@ -15,7 +16,7 @@ from Crypto.PublicKey import ECC from dbus_next import DBusError, Variant import claim -from pairing import ensure_store, load_keys, load_phones +from pairing import ensure_store, load_keys, load_phones, remove_phone from protocol import Assembler, packets, seal from server import Gateway @@ -160,6 +161,39 @@ class ClaimGatewayTests(unittest.IsolatedAsyncioTestCase): self.assertFalse(state2.response) self.assertEqual(len(load_phones(self.keys)), 1) + async def test_key_refresh_during_claim_keeps_reply_session(self): + claim.arm("AA:BB:CC:DD:EE:FF", "comma", self.claim) + private, data = phone_request() + + async def interleaved_work(function, *args): + result = function(*args) + self.gateway.refresh_key() + return result + + with patch("server.asyncio.to_thread", side_effect=interleaved_work): + state = await self.send(data) + self.assertFalse(state.closed) + self.assertIs(self.gateway.sessions[self.options["device"].value], state) + reply = phone_open(private, self.read_reply(state), state.challenge) + self.assertEqual(load_keys(self.keys)[reply["id"]].hex(), reply["key"]) + + async def test_key_removed_during_claim_is_not_delivered(self): + claim.arm("AA:BB:CC:DD:EE:FF", "comma", self.claim) + _, data = phone_request() + + async def interleaved_work(function, *args): + result = function(*args) + if function.__name__ == "add_phone": + remove_phone(result[0], self.keys) + self.gateway.refresh_key() + return result + + with patch("server.asyncio.to_thread", side_effect=interleaved_work): + state = await self.send(data) + self.assertTrue(state.closed) + self.assertIsNone(state.phone) + self.assertFalse(state.response) + async def test_music_bond_made_first_links_the_claimed_phone(self): claim.arm("AA:BB:CC:DD:EE:FF", "comma", self.claim) claim.note_paired("11:22:33:44:55:66", self.claim) diff --git a/openpilot/starpilot/galaxy_bluetooth/tests/test_phone_ui.py b/openpilot/starpilot/galaxy_bluetooth/tests/test_phone_ui.py index 8204a5a7b0..23e3f48837 100644 --- a/openpilot/starpilot/galaxy_bluetooth/tests/test_phone_ui.py +++ b/openpilot/starpilot/galaxy_bluetooth/tests/test_phone_ui.py @@ -167,5 +167,26 @@ class PermissionTest(unittest.TestCase): self.assertEqual(list(pairing.load_keys(path)), [linked]) + def test_app_pairing_tiles_show_the_name_and_last_use_not_the_added_date(self): + one = {'label': 'iPhone', 'added': '2026-10-16T14:02:00', 'last_used': '2026-10-20'} + two = {'label': 'Pixel', 'added': '2026-10-01T09:00:00', 'last_used': ''} + self.assertEqual(phone_ui.app_pairing_title(one, [one, two]), 'iPhone') + self.assertEqual(phone_ui.app_pairing_subtitle(one), 'last used Oct 20') + self.assertEqual(phone_ui.app_pairing_subtitle(two), 'not used yet') + twin = {'label': 'iPhone', 'added': '2026-10-05T08:00:00', 'last_used': ''} + self.assertEqual(phone_ui.app_pairing_title(twin, [one, twin]), 'iPhone (Oct 5)') + + def test_one_app_pairing_can_be_forgotten_on_its_own(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / 'pairing.json' + first, _ = pairing.add_phone('iPhone', path) + second, _ = pairing.add_phone('Pixel', path) + with patch.object(phone_ui, 'remove_phone', side_effect=lambda phone_id: pairing.remove_phone(phone_id, path)): + phone_ui.forget_app_phone(lambda: False, first) + self.assertEqual(len(pairing.load_keys(path)), 2) + phone_ui.forget_app_phone(lambda: True, first) + self.assertEqual(list(pairing.load_keys(path)), [second]) + + if __name__ == '__main__': unittest.main() diff --git a/openpilot/starpilot/system/android_auto/bluetooth_bridge.py b/openpilot/starpilot/system/android_auto/bluetooth_bridge.py index f091c53457..6fde287819 100644 --- a/openpilot/starpilot/system/android_auto/bluetooth_bridge.py +++ b/openpilot/starpilot/system/android_auto/bluetooth_bridge.py @@ -319,7 +319,7 @@ class SharedBluetoothOwner(BluetoothOwner): self.companion_devices.clear() self.phone_lease = None window = self.pairing - if isinstance(window, OpenPairingSession) and not window.done.is_set(): + if window is not None and not window.done.is_set(): window.cancel() window.done.wait(5.0) if lease.session is not None: @@ -327,6 +327,7 @@ class SharedBluetoothOwner(BluetoothOwner): try: try: try: + self._stop_scan() if lease.discovering: try: self._client().operation('stop_scan') @@ -646,15 +647,18 @@ class SharedBluetoothOwner(BluetoothOwner): self.companion_devices.pop(address, None) return self.snapshot(session=session) hfp = '0000111e-0000-1000-8000-00805f9b34fb' - if device is None or not device['paired'] or hfp not in device.get('uuids', []): - raise BluetoothRejected('Choose an already paired hands-free car', code='changed') + if device is None or not device['paired']: + raise BluetoothRejected('Choose an already paired device', code='changed') if not device['path'].startswith(lease.adapter_path + '/'): raise BluetoothRejected('Bluetooth adapter changed', code='changed') + is_car = hfp in device.get('uuids', []) was_admitted = self.companion_devices.get(address) - if operation == 'connect': + if operation == 'connect' and is_car: self.companion_devices[address] = device['path'] try: - if operation == 'connect': + if not is_car: + client.operation(operation, address) + elif operation == 'connect': lease.phone._call(device['path'], 'org.bluez.Device1', 'ConnectProfile', 's', (hfp,), timeout=12.0) else: # Disconnect only the companion car, never the projection adapter. @@ -679,27 +683,69 @@ class SharedBluetoothOwner(BluetoothOwner): finally: self.lock.release() + def companion_power(self, enabled: bool, session: tuple) -> dict | None: + """Turn the radio off (or confirm it is on) from Galaxy while Android Auto holds the adapter. + + None means no lease holds it, so the caller's own owner handles the request. Turning it off releases the + projection lease first (see request), so Android Auto lets go of the adapter before the radio stops. + """ + lease = self.phone_lease + if lease is None or lease.released: + return None + if not self.session_valid(session): + raise BluetoothRejected('Galaxy session expired', code='session_expired') + if lease.session is not None: + raise BluetoothRejected('Finish Android Auto pairing first', code='busy') + if enabled: + return self.snapshot(session=session) + return self.request('power', enabled=False, session=session) + def phone_window(self, operation: str, session: tuple, *, prompt_id: str | None = None, - accepted: bool | None = None, value: str = '') -> dict | None: + accepted: bool | None = None, value: str = '', address: str | None = None) -> dict | None: """Open, answer or read a phone pairing window inside the projection lease; None means no projection holds the adapter.""" - if operation not in ('accept_pair', 'pairing_response', 'cancel_pair', 'status'): + if operation not in ('accept_pair', 'pair', 'scan', 'stop_scan', 'pairing_response', 'cancel_pair', 'status'): raise ValueError('Unsupported phone window operation') if not self.lock.acquire(blocking=False): raise BluetoothRejected('Bluetooth is busy', code='busy') try: lease = self.phone_lease - window = self.pairing if isinstance(self.pairing, OpenPairingSession) else None + window = self.pairing if lease is None or lease.released: return None if lease.session is not None: raise BluetoothRejected('Finish Android Auto pairing first', code='busy') if not self.session_valid(session): raise BluetoothRejected('Galaxy session expired', code='session_expired') - if operation != 'accept_pair' and (window is None or window.identity != session): + if operation in ('status', 'cancel_pair', 'pairing_response') and (window is None or window.identity != session): return None - if operation == 'accept_pair': + if operation != 'status' and not self.parked(): + raise BluetoothRejected('Park before changing Bluetooth devices', code='park_required') + if operation in ('scan', 'stop_scan'): + return super().request(operation, session=session) + if operation == 'pair': + address = normalized_address(address) + if address == lease.receiver: + raise BluetoothRejected('Use Android Auto to pair its receiver', code='projection_active') + self._end_idle_window() + if self.pairing is not None and not self.pairing.done.is_set(): + raise BluetoothRejected('Pairing is in progress', code='busy') + client = self._client() + path, _, sender = client.adapter_identity() + device = next((d for d in client._devices(client._objects()) if d['address'] == address), None) + if (path, sender) != (lease.adapter_path, lease.bluez_sender) or device is None or device['paired'] or not device['path'].startswith(path + '/'): + raise BluetoothRejected('Device changed before pairing', code='changed') + self._stop_scan() + window = PairingSession(session, address, device['path'], + lambda: self.phone_lease is lease and not lease.released and self.parked() and self.session_valid(session), self.clock) + self.pairing = window + self.window_paired = None + threading.Thread(target=self._pair_worker, args=(window,), daemon=True).start() + threading.Thread(target=self._watch_pair, args=(window,), daemon=True).start() + elif operation == 'accept_pair': if not self.parked(): raise BluetoothRejected('Park before pairing a phone', code='park_required') + # A window nobody has claimed yet gives way to a new one; a claimed one still blocks. + self._end_idle_window() if self.pairing is not None and not self.pairing.done.is_set(): raise BluetoothRejected('Pairing is in progress', code='busy') window = OpenPairingSession(session, lambda: self.session_valid(session) and self.parked(), self.clock) diff --git a/openpilot/starpilot/system/android_auto/daemon.py b/openpilot/starpilot/system/android_auto/daemon.py index e74769df7f..39df0de9c3 100644 --- a/openpilot/starpilot/system/android_auto/daemon.py +++ b/openpilot/starpilot/system/android_auto/daemon.py @@ -30,7 +30,7 @@ from openpilot.starpilot.system.android_auto.supervisor import Supervisor PAIRING_COMMANDS = {"prepare_pairing", "pairing_status", "pairing_response", "cancel_pairing", "pair_device"} AUTHENTICATED_COMMANDS = {"start", "set_auto_connect", "select_receiver", "set_view", "set_connection", "prepare_pairing", "pairing_status", "pairing_response", "cancel_pairing", "pair_device", "devices", "bluetooth_action", - "phone_window"} + "phone_window", "bluetooth_power"} def _offroad() -> bool: @@ -59,7 +59,7 @@ def handle(supervisor: Supervisor, request: dict[str, Any], verifier: GalaxySour source = request.get('source') if not isinstance(source, str) or verifier is None or not verifier.valid(source): raise RuntimeError('Authenticated Galaxy Android Auto source required') - if command not in ('bluetooth_action', 'phone_window'): + if command not in ('bluetooth_action', 'phone_window', 'bluetooth_power'): supervisor.bind_source_session(('galaxy', source)) if command in PAIRING_COMMANDS and not (parked or _offroad)(): raise RuntimeError("Disengage and shift into Park before pairing") @@ -72,23 +72,37 @@ def handle(supervisor: Supervisor, request: dict[str, Any], verifier: GalaxySour try: result = supervisor.companion_bluetooth_action(request['operation'], request['address'], ('galaxy', request['source'])) except (BluetoothRejected, BluetoothUnavailable) as error: - return {'handled': True, 'error_code': error.code} + return {'handled': True, 'error_code': error.code, 'message': str(error)[:160]} + except (OSError, RuntimeError): + return {'handled': True, 'error_code': 'service_unavailable'} + return {'handled': result is not None, **({'bluetooth': result} if result is not None else {})} + if command == 'bluetooth_power': + from openpilot.starpilot.bluetooth.owner import BluetoothRejected, BluetoothUnavailable + if set(request) != {'command', 'source', 'enabled'} or type(request['enabled']) is not bool: + raise ValueError('Invalid companion Bluetooth power request') + if not (parked or _offroad)(): + return {'handled': True, 'error_code': 'park_required'} + try: + result = supervisor.companion_power(request['enabled'], ('galaxy', request['source'])) + except (BluetoothRejected, BluetoothUnavailable) as error: + return {'handled': True, 'error_code': error.code, 'message': str(error)[:160]} except (OSError, RuntimeError): return {'handled': True, 'error_code': 'service_unavailable'} return {'handled': result is not None, **({'bluetooth': result} if result is not None else {})} if command == 'phone_window': from openpilot.starpilot.bluetooth.owner import BluetoothRejected, BluetoothUnavailable operation = request.get('operation') - fields = {'command', 'source', 'operation'} | ({'prompt_id', 'accepted', 'value'} if operation == 'pairing_response' else set()) - if set(request) != fields or operation not in ('accept_pair', 'pairing_response', 'cancel_pair', 'status'): + fields = {'command', 'source', 'operation'} + fields |= {'prompt_id', 'accepted', 'value'} if operation == 'pairing_response' else {'address'} if operation == 'pair' else set() + if set(request) != fields or operation not in ('accept_pair', 'pair', 'scan', 'stop_scan', 'pairing_response', 'cancel_pair', 'status'): raise ValueError('Invalid phone pairing request') if operation != 'status' and not (parked or _offroad)(): return {'handled': True, 'error_code': 'park_required'} - answer = {key: request[key] for key in ('prompt_id', 'accepted', 'value') if key in request} + answer = {key: request[key] for key in ('prompt_id', 'accepted', 'value', 'address') if key in request} try: result = supervisor.phone_window(operation, ('galaxy', request['source']), **answer) except (BluetoothRejected, BluetoothUnavailable) as error: - return {'handled': True, 'error_code': error.code} + return {'handled': True, 'error_code': error.code, 'message': str(error)[:160]} except (OSError, RuntimeError): return {'handled': True, 'error_code': 'service_unavailable'} return {'handled': result is not None, **({'bluetooth': result} if result is not None else {})} diff --git a/openpilot/starpilot/system/android_auto/projection_onroad.py b/openpilot/starpilot/system/android_auto/projection_onroad.py index 9e43faff74..2fb000f725 100644 --- a/openpilot/starpilot/system/android_auto/projection_onroad.py +++ b/openpilot/starpilot/system/android_auto/projection_onroad.py @@ -54,6 +54,17 @@ def certificate_notice(days_left: int | None) -> str: return f"Heads Up: AA Certificate Expires {when}" +def apply_plain_camera_look(camera, rl): + """The car screen keeps the plain camera look: the shader's non-road path, not the comma UI's engagement grading. + + The comma UI fades the road camera when disengaged (color_strength) and a car screen reads that as washed out. + """ + camera._color_strength_val[0] = 1.0 + camera._road_camera_val[0] = 0 + rl.set_shader_value(camera.shader, camera._color_strength_loc, camera._color_strength_val, rl.ShaderUniformDataType.SHADER_UNIFORM_FLOAT) + rl.set_shader_value(camera.shader, camera._road_camera_loc, camera._road_camera_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT) + + def native_dependencies(): import pyray as rl from openpilot.cereal.visionipc import VisionStreamType @@ -105,12 +116,7 @@ def native_dependencies(): def _update_texture_color_filtering(self): if getattr(self, 'large_ui_gamma_trial', False): return super()._update_texture_color_filtering() - # The comma UI fades the engaged road camera to 20% color (30477f3cef). On the car - # screen that reads as black and white, so keep the plain look: non-road shader path. - self._engaged_val[0] = 0 - self._road_camera_val[0] = 0 - rl.set_shader_value(self.shader, self._engaged_loc, self._engaged_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT) - rl.set_shader_value(self.shader, self._road_camera_loc, self._road_camera_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT) + apply_plain_camera_look(self, rl) def standby_logo(height): """The StarPilot crescent, shrunk once on the CPU to its standby size; None if it cannot be loaded.""" diff --git a/openpilot/starpilot/system/android_auto/protocol.py b/openpilot/starpilot/system/android_auto/protocol.py index 9fd7a8cafb..74d2dafdb3 100644 --- a/openpilot/starpilot/system/android_auto/protocol.py +++ b/openpilot/starpilot/system/android_auto/protocol.py @@ -8,7 +8,9 @@ import socket from typing import Any ANDROID_AUTO_SOCKET_PATH = "/tmp/starpilot-android-auto.sock" -COMMAND_TIMEOUTS = {"stop": 12.0, "prepare_pairing": 20.0, "devices": 10.0, "select_receiver": 10.0, "bluetooth_action": 25.0, +COMMAND_TIMEOUTS = {"stop": 12.0, "prepare_pairing": 20.0, "devices": 10.0, "select_receiver": 10.0, "bluetooth_action": 25.0, "bluetooth_power": 25.0, + # Opening a window may wait on a slow BlueZ right after boot, and a closed one is given time to wind down. + "phone_window": 25.0, "forget_receiver": 15.0} diff --git a/openpilot/starpilot/system/android_auto/source_verifier.py b/openpilot/starpilot/system/android_auto/source_verifier.py index 0296a3edcb..017eb7486f 100644 --- a/openpilot/starpilot/system/android_auto/source_verifier.py +++ b/openpilot/starpilot/system/android_auto/source_verifier.py @@ -8,7 +8,7 @@ SOURCE_ID = re.compile(r'[0-9a-f]{64}\Z') class GalaxySourceVerifier: - def __init__(self, host: str = '127.0.0.1', port: int = 8082, timeout: float = 1.0): + def __init__(self, host: str = '127.0.0.1', port: int = 8082, timeout: float = 3.0): self.host, self.port, self.timeout = host, port, timeout def valid(self, source: str) -> bool: diff --git a/openpilot/starpilot/system/android_auto/supervisor.py b/openpilot/starpilot/system/android_auto/supervisor.py index f26374e3ca..2ea8e7550c 100644 --- a/openpilot/starpilot/system/android_auto/supervisor.py +++ b/openpilot/starpilot/system/android_auto/supervisor.py @@ -778,6 +778,17 @@ class Supervisor: owner = self._shared_bluetooth_owner return None if owner is None else owner.phone_window(operation, session, **arguments) + def companion_power(self, enabled: bool, session: tuple): + owner = self._shared_bluetooth_owner + if owner is None: + return None + if not enabled and self._session_alive(): + from openpilot.starpilot.bluetooth.owner import BluetoothRejected + raise BluetoothRejected('Stop Android Auto before turning Bluetooth off', code='projection_active') + # Same lock order as select_receiver: supervisor first, then the owner releases its lease. + with self._lock: + return owner.companion_power(enabled, session) + def companion_bluetooth_action(self, operation: str, address: str, session: tuple): owner = self._shared_bluetooth_owner if owner is None: @@ -802,6 +813,8 @@ class Supervisor: owner.phone_lease is not lease or lease.released or not owner.session_valid(session) or not lease.enabled()): raise RuntimeError('Selected car connection was not confirmed') + if '0000111e-0000-1000-8000-00805f9b34fb' not in device.get('uuids', []): + return result # An accessory must never replace the dongle's selected car. self.config.update(companion_address=device['address'], companion_name=device['name']) identity_store.save_config(self.config) for previous in list(owner.companion_devices): diff --git a/openpilot/starpilot/system/android_auto/tests/test_bluetooth_bridge.py b/openpilot/starpilot/system/android_auto/tests/test_bluetooth_bridge.py index bfbf18f3eb..db9767c1cf 100644 --- a/openpilot/starpilot/system/android_auto/tests/test_bluetooth_bridge.py +++ b/openpilot/starpilot/system/android_auto/tests/test_bluetooth_bridge.py @@ -299,6 +299,62 @@ def test_projection_survives_drive_and_browser_expiry_but_not_disable_or_receive shared.close() +def test_accessory_scan_and_pair_keep_projection_lock_and_cancel_on_release(tmp_path): + import threading + state = {'parked': True, 'session': ('galaxy', '42'), 'enabled': True, 'selected': 'AA:BB:CC:DD:EE:FF'} + bluez = FakeBlueZ() + target = '11:22:33:44:55:66' + bluez.paired.append({'address': target, 'path': '/org/bluez/hci0/dev_11_22_33_44_55_66', + 'paired': False, 'connected': False, 'name': 'Car'}) + shared = owner(tmp_path, state, bluez) + lease = shared.acquire_projection_role(state['selected'], Phone(), enabled=lambda: state['enabled'], + selected_receiver=lambda: state['selected']) + other = owner(tmp_path, state, bluez) + started = threading.Event() + def pair(address, path, session): + started.set() + session.done.wait(2) + return False + bluez.pair = pair + bluez.cancel_pair = lambda path: None + try: + assert shared.phone_window('scan', state['session'])['discovering'] + with pytest.raises(BluetoothRejected, match='another process'): + other.request('scan') + result = shared.phone_window('pair', state['session'], address=target) + assert result['pairing']['address'] == target + assert started.wait(1) + assert not bluez.props['Discovering'] + assert shared.phone_lease is lease and not lease.released + assert shared.phone_window('status', state['session'])['pairing']['state'] == 'pairing' + lease.release() + assert shared.pairing.done.wait(1) + assert shared.pairing.state == 'canceled' + assert other.request('scan')['discovering'] + finally: + shared.close() + other.close() + + +def test_projection_accessory_operations_require_park_and_reject_receiver_pairing(tmp_path): + state = {'parked': True, 'session': ('galaxy', '42')} + bluez = FakeBlueZ() + shared = owner(tmp_path, state, bluez) + receiver = bluez.paired[0]['address'] + shared.acquire_projection_role(receiver, Phone(), enabled=lambda: True, selected_receiver=lambda: receiver) + try: + with pytest.raises(BluetoothRejected, match='Use Android Auto'): + shared.phone_window('pair', state['session'], address=receiver) + state['parked'] = False + for operation in ('scan', 'pair'): + with pytest.raises(BluetoothRejected) as error: + shared.phone_window(operation, state['session'], address='11:22:33:44:55:66') + assert error.value.code == 'park_required' + assert not bluez.props['Discovering'] + finally: + shared.close() + + def test_projection_restores_original_visibility_on_release_and_failure(tmp_path): state = {'parked': True, 'session': ('galaxy', '42')} bluez = FakeBlueZ() diff --git a/openpilot/starpilot/system/android_auto/tests/test_companion_bluetooth.py b/openpilot/starpilot/system/android_auto/tests/test_companion_bluetooth.py index 6cb51b3f72..941e53a173 100644 --- a/openpilot/starpilot/system/android_auto/tests/test_companion_bluetooth.py +++ b/openpilot/starpilot/system/android_auto/tests/test_companion_bluetooth.py @@ -60,8 +60,8 @@ class CompanionTests(unittest.TestCase): self.assertTrue(self.devices[ADAPTER]['connected']) self.assertFalse(self.lease.released) - def test_selected_adapter_and_non_hfp_or_unpaired_peers_never_change_radio(self): - for address, alteration in [(ADAPTER, {}), (CAR, {'paired': False}), (CAR, {'uuids': []}), + def test_selected_adapter_and_unpaired_or_foreign_peers_never_change_radio(self): + for address, alteration in [(ADAPTER, {}), (CAR, {'paired': False}), (CAR, {'path': '/org/bluez/hci1/dev_car'})]: before = dict(self.devices[address]) self.devices[address].update(alteration) @@ -71,6 +71,27 @@ class CompanionTests(unittest.TestCase): self.assertEqual(self.calls, []) self.assertEqual(self.owner.companion_devices, {}) + def test_accessory_connect_preserves_the_selected_car_and_projection(self): + from openpilot.starpilot.system.android_auto.supervisor import Supervisor + self.devices[CAR]['uuids'] = [] # HID controller, not an HFP car. + def operation(operation, address): + self.forgotten.append((operation, address)) + self.devices[address]['connected'] = operation == 'connect' + self.client.operation = operation + host = Supervisor.__new__(Supervisor) + host._lock = threading.RLock() + host.config = {'receiver_address': ADAPTER, 'companion_address': 'saved-car', 'companion_name': 'My car'} + host._shared_bluetooth_owner = self.owner + host._phone = lambda: self.phone + with patch('openpilot.starpilot.system.android_auto.identity.save_config') as save: + host.companion_bluetooth_action('connect', CAR, SESSION) + host.companion_bluetooth_action('disconnect', CAR, SESSION) + save.assert_not_called() + self.assertEqual(self.forgotten, [('connect', CAR), ('disconnect', CAR)]) + self.assertEqual(host.config['companion_address'], 'saved-car') + self.assertEqual(self.owner.companion_devices, {}) + self.assertFalse(self.lease.released) + def test_session_pairing_and_changed_owner_are_fail_closed(self): alterations = [('valid', False), ('enabled', False), ('selected', CAR)] for key, value in alterations: @@ -275,15 +296,61 @@ class CompanionTests(unittest.TestCase): self.assertEqual(self.daemon_call(), {'handled': False}) def test_daemon_denials_and_timeouts_never_produce_normal_owner_fallback(self): - self.assertEqual(self.daemon_call(address=ADAPTER), {'handled': True, 'error_code': 'projection_active'}) + self.assertEqual(self.daemon_call(address=ADAPTER), {'handled': True, 'error_code': 'projection_active', + 'message': 'Stop projection before changing its adapter'}) self.lease.session = SESSION - self.assertEqual(self.daemon_call(), {'handled': True, 'error_code': 'busy'}) + self.assertEqual(self.daemon_call(), {'handled': True, 'error_code': 'busy', 'message': 'Finish Android Auto pairing first'}) self.lease.session = None def timeout(): raise TimeoutError('No profile reply') self.hook = timeout self.assertEqual(self.daemon_call(), {'handled': True, 'error_code': 'service_unavailable'}) self.assertFalse(self.lease.released) + def test_power_off_goes_through_the_owner_and_power_on_is_a_no_op(self): + self.owner.request = Mock(return_value={'powered': False}) # ty: ignore[invalid-assignment] # test double + self.assertEqual(self.owner.companion_power(False, SESSION), {'powered': False}) + self.owner.request.assert_called_once_with('power', enabled=False, session=SESSION) + self.owner.request.reset_mock() + result = self.owner.companion_power(True, SESSION) + assert result is not None + self.assertTrue(result['powered']) + self.owner.request.assert_not_called() + + def test_power_refusals_and_no_lease(self): + self.state['valid'] = False + with self.assertRaises(Rejected) as error: + self.owner.companion_power(False, SESSION) + self.assertEqual(error.exception.code, 'session_expired') + self.state['valid'] = True + self.lease.session = SESSION + with self.assertRaises(Rejected) as error: + self.owner.companion_power(False, SESSION) + self.assertEqual(error.exception.code, 'busy') + self.lease.session = None + self.owner.phone_lease = None + self.assertIsNone(self.owner.companion_power(False, SESSION)) + + def power_call(self, enabled=False, valid=True, parked=True): + supervisor = SimpleNamespace(companion_power=self.owner.companion_power, + bind_source_session=lambda session: self.fail('Companion command must not rebind projection source')) + request = {'command': 'bluetooth_power', 'source': SESSION[1], 'enabled': enabled} + with patch('openpilot.common.params.Params') as params: + params.return_value.get_bool.return_value = True + return daemon.handle(Mock(wraps=supervisor), request, Mock(wraps=SimpleNamespace(valid=lambda source: valid)), parked=lambda: parked) + + def test_daemon_power_command_requires_proof_park_and_a_boolean(self): + self.owner.request = Mock(return_value={'powered': False}) # ty: ignore[invalid-assignment] # test double + self.assertEqual(self.power_call(), {'handled': True, 'bluetooth': {'powered': False}}) + self.assertEqual(self.power_call(parked=False), {'handled': True, 'error_code': 'park_required'}) + with self.assertRaises(RuntimeError): + self.power_call(valid=False) + with self.assertRaises(ValueError): + self.power_call(enabled='off') # ty: ignore[invalid-argument-type] # wrong type on purpose + self.lease.session = SESSION + self.assertEqual(self.power_call(), {'handled': True, 'error_code': 'busy', 'message': 'Finish Android Auto pairing first'}) + self.owner.phone_lease = None + self.assertEqual(self.power_call(), {'handled': False}) + def test_daemon_requires_live_source_and_park_before_touching_owner(self): with self.assertRaises(RuntimeError): self.daemon_call(valid=False) diff --git a/openpilot/starpilot/system/android_auto/tests/test_phone_window.py b/openpilot/starpilot/system/android_auto/tests/test_phone_window.py index 042313d618..39be21b6fb 100644 --- a/openpilot/starpilot/system/android_auto/tests/test_phone_window.py +++ b/openpilot/starpilot/system/android_auto/tests/test_phone_window.py @@ -36,9 +36,20 @@ class PhoneWindowTest(unittest.TestCase): process_lock=Mock(), receiver=CAR) self.owner.phone_lease = self.lease # ty: ignore[invalid-assignment] # test double + def window_call(self, operation): + # A closed window's watcher briefly holds the owner lock while it lets go of the adapter. + for _ in range(300): + try: + return self.owner.phone_window(operation, SESSION) + except BluetoothRejected as error: + if str(error) != 'Bluetooth is busy': + raise + time.sleep(0.01) + self.fail('owner stayed busy') + def claimed(self): for _ in range(300): - result = self.owner.phone_window('status', SESSION) + result = self.window_call('status') if result and result['pairing'] and result['pairing']['prompt']: return result['pairing'] time.sleep(0.01) @@ -61,6 +72,20 @@ class PhoneWindowTest(unittest.TestCase): self.assertFalse(self.lease.released) self.assertEqual(self.owner.journal_path.read_text(), '{"lease": "original visibility"}') + def test_unclaimed_window_gives_way_to_a_new_one_but_a_claimed_window_blocks(self): + self.assertIsNotNone(self.window_call('accept_pair')) + first = self.owner.pairing + assert first is not None + self.claimed() + first.path = '' # nobody has claimed it + self.assertIsNotNone(self.window_call('accept_pair')) + self.assertTrue(first.done.is_set()) + self.assertIsNot(self.owner.pairing, first) + self.claimed() + with self.assertRaises(BluetoothRejected) as error: + self.window_call('accept_pair') + self.assertEqual((error.exception.code, str(error.exception)), ('busy', 'Pairing is in progress')) + def test_no_projection_means_not_handled(self): self.owner.phone_lease = None for operation in ('accept_pair', 'status', 'cancel_pair'): @@ -104,5 +129,13 @@ class PhoneWindowTest(unittest.TestCase): self.assertFalse(self.owner.journal_path.exists()) +class TimeoutTest(unittest.TestCase): + def test_opening_a_window_and_proving_the_source_get_time_after_boot(self): + from openpilot.starpilot.system.android_auto.protocol import COMMAND_TIMEOUTS + from openpilot.starpilot.system.android_auto.source_verifier import GalaxySourceVerifier + self.assertGreaterEqual(COMMAND_TIMEOUTS['phone_window'], 20.0) + self.assertGreaterEqual(GalaxySourceVerifier().timeout, 3.0) + + if __name__ == '__main__': unittest.main() diff --git a/openpilot/starpilot/system/android_auto/tests/test_projection_plain_look.py b/openpilot/starpilot/system/android_auto/tests/test_projection_plain_look.py new file mode 100644 index 0000000000..9c8334d699 --- /dev/null +++ b/openpilot/starpilot/system/android_auto/tests/test_projection_plain_look.py @@ -0,0 +1,21 @@ +"""The car screen's camera look follows the shader's current uniforms (color_strength and road_camera).""" +from types import SimpleNamespace +from unittest.mock import Mock + +from openpilot.starpilot.system.android_auto.projection_onroad import apply_plain_camera_look + + +def test_car_screen_uses_the_plain_non_road_path_with_the_current_shader_uniforms(): + rl = SimpleNamespace(set_shader_value=Mock(), ShaderUniformDataType=SimpleNamespace(SHADER_UNIFORM_FLOAT='float', SHADER_UNIFORM_INT='int')) + camera = SimpleNamespace(shader='shader', _color_strength_val=[0.2], _color_strength_loc=3, _road_camera_val=[1], _road_camera_loc=4) + apply_plain_camera_look(camera, rl) + assert camera._color_strength_val == [1.0] and camera._road_camera_val == [0] + assert [call.args for call in rl.set_shader_value.call_args_list] == [ + ('shader', 3, [1.0], 'float'), ('shader', 4, [0], 'int')] + + +def test_it_needs_none_of_the_removed_engaged_uniform(): + rl = SimpleNamespace(set_shader_value=Mock(), ShaderUniformDataType=SimpleNamespace(SHADER_UNIFORM_FLOAT='float', SHADER_UNIFORM_INT='int')) + camera = SimpleNamespace(shader='shader', _color_strength_val=[0.0], _color_strength_loc=3, _road_camera_val=[1], _road_camera_loc=4) + assert not hasattr(camera, '_engaged_val') + apply_plain_camera_look(camera, rl) diff --git a/openpilot/starpilot/ui/bluetooth_compact.py b/openpilot/starpilot/ui/bluetooth_compact.py index b62c62f3aa..5dc9d41011 100644 --- a/openpilot/starpilot/ui/bluetooth_compact.py +++ b/openpilot/starpilot/ui/bluetooth_compact.py @@ -8,7 +8,8 @@ import uuid from openpilot.selfdrive.ui.mici.widgets.button import BigButton, GreyBigButton from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog, BigInputDialog -from openpilot.starpilot.bluetooth.owner import BluetoothOwner, BluetoothRejected, BluetoothUnavailable +from openpilot.starpilot.bluetooth.owner import BluetoothRejected, BluetoothUnavailable +from openpilot.starpilot.bluetooth.native_owner import NativeBluetoothOwner as BluetoothOwner from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets.scroller import NavScroller @@ -123,6 +124,8 @@ class BluetoothCompact(NavScroller): prompt_id=prompt_id, accepted=accepted, value=value) elif operation in ("connect", "disconnect", "forget"): self.pending = self.executor.submit(self._device_request, operation, address) + elif operation == "power": + self.pending = self.executor.submit(self._power_request, bool(enabled)) else: self.pending = self.executor.submit(self.owner.request, operation, address=address, enabled=enabled) self.operation = operation @@ -131,18 +134,11 @@ class BluetoothCompact(NavScroller): self.return_to_root = return_to_root self._rebuild() + def _power_request(self, enabled: bool) -> dict: + return self.owner.request("power", address=None, enabled=enabled) + def _device_request(self, operation: str, address: str | None) -> dict: - try: - return self.owner.request(operation, address=address) - except BluetoothRejected as error: - if error.code != "busy": - raise - # Android Auto holds the adapter in its own process; Galaxy hands the change to it. - from openpilot.starpilot.galaxy_bluetooth.phone_ui import ACTION, galaxy_call - status, result = galaxy_call()("POST", ACTION, {"operation": operation, "address": address}) - if status != 200 or not isinstance(result.get("devices"), list): - raise BluetoothRejected("Android Auto could not complete the Bluetooth change") - return result + return self.owner.request(operation, address=address) def _tick(self): if self.pending is not None and self.pending.done(): @@ -266,7 +262,9 @@ class BluetoothCompact(NavScroller): pairing.set_enabled(lambda: self._ready() or self._pairing_ready()) cards.append(pairing) if status.get("powered"): - devices = [device for device in status.get("devices", ()) if device.get("paired") or device.get("connected")] + from openpilot.starpilot.galaxy_bluetooth.phone_ui import is_app_link + devices = [device for device in status.get("devices", ()) if (device.get("paired") or device.get("connected")) and not is_app_link(device)] + links = [device for device in status.get("devices", ()) if is_app_link(device)] cards.append(GreyBigButton("your devices", "Bluetooth connections")) for device in devices: value = "connected" if device.get("connected") else "disconnected" @@ -274,12 +272,23 @@ class BluetoothCompact(NavScroller): button.set_click_callback(lambda addr=device["address"]: self._open_device(addr)) button.set_enabled(self._ready) cards.append(button) - from openpilot.starpilot.galaxy_bluetooth.bridge.pairing import describe - phones = self._app_only() - for phone in phones: - cards.append(GreyBigButton(describe(phone).replace("·", "-"), "app only, music not linked")) - if not devices and not phones: + if not devices: cards.append(GreyBigButton("saved devices", "no saved pairings")) + from openpilot.starpilot.galaxy_bluetooth.phone_ui import app_pairing_subtitle, app_pairing_title + phones = self._app_only() + if links or phones: + cards.append(GreyBigButton("app pairings", "phones using the StarPilot app")) + for device in links: + button = BigButton(device_label(device), "app connected", scroll=True) + button.set_click_callback(lambda addr=device["address"]: self._open_device(addr)) + button.set_enabled(self._ready) + cards.append(button) + for phone in phones: + tile = BigButton(app_pairing_title(phone, phones).replace("·", "-"), app_pairing_subtitle(phone), scroll=True) + tile.set_enabled(lambda: self.phone_pairing_allowed()) + tile.set_click_callback(lambda phone_id=phone["id"]: gui_app.push_widget(BigConfirmationDialog( + "forget this app pairing", self.icon, lambda: self._forget_app_phone(phone_id), red=True))) + cards.append(tile) if self.pairing_page is not None and gui_app.widget_in_stack(self.pairing_page): self._rebuild_pairing() self._scroller.items.clear() @@ -323,7 +332,7 @@ class BluetoothCompact(NavScroller): button.set_enabled(self._ready) cards.append(button) if self._app_only(): - cleanup = BigButton("forget app keys", "app-only phones") + cleanup = BigButton("forget all app pairings", "app-only phones") cleanup.set_enabled(lambda: self.phone_pairing_allowed()) cleanup.set_click_callback(lambda: gui_app.push_widget(BigConfirmationDialog( "Remove phones paired for the StarPilot app only?", self.icon, @@ -332,6 +341,11 @@ class BluetoothCompact(NavScroller): self.pairing_page._scroller.items.clear() self.pairing_page._scroller.add_widgets(cards) + def _forget_app_phone(self, phone_id: str): + from openpilot.starpilot.galaxy_bluetooth.phone_ui import forget_app_phone + forget_app_phone(self.phone_pairing_allowed, phone_id) + self._rebuild() + def _forget_app_only(self): from openpilot.starpilot.galaxy_bluetooth.phone_ui import forget_app_only forget_app_only(self.phone_pairing_allowed) @@ -344,6 +358,11 @@ class BluetoothCompact(NavScroller): device = next((item for item in (self.status or {}).get("devices", ()) if item.get("address") == address), None) if device is None: return + from openpilot.starpilot.galaxy_bluetooth.phone_ui import is_app_link + if is_app_link(device): + # A phone running the StarPilot app, not a Bluetooth pairing: the only thing to do here is let go of it. + gui_app.push_widget(BigConfirmationDialog("forget this app connection", self.icon, lambda: self._forget(address), red=True)) + return if not device.get("paired"): self._request("pair", address=address) return diff --git a/openpilot/starpilot/ui/bluetooth_large.py b/openpilot/starpilot/ui/bluetooth_large.py index 237082cf9d..6518960f8d 100644 --- a/openpilot/starpilot/ui/bluetooth_large.py +++ b/openpilot/starpilot/ui/bluetooth_large.py @@ -5,8 +5,9 @@ import time import uuid from openpilot.starpilot.galaxy_bluetooth.bootstrap import setup_status -from openpilot.starpilot.galaxy_bluetooth.phone_ui import forget_phone_keys, large_phone_rows -from openpilot.starpilot.bluetooth.owner import BluetoothOwner, BluetoothRejected, BluetoothUnavailable +from openpilot.starpilot.galaxy_bluetooth.phone_ui import forget_phone_keys, is_app_link, large_phone_rows +from openpilot.starpilot.bluetooth.owner import BluetoothRejected, BluetoothUnavailable +from openpilot.starpilot.bluetooth.native_owner import NativeBluetoothOwner as BluetoothOwner from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog @@ -132,6 +133,11 @@ class BluetoothLarge(Widget): if operation in ("pair", "pairing_response", "cancel_pair"): self.pending = self.executor.submit(self.owner.request, operation, address=address, session=session, prompt_id=prompt_id, accepted=accepted, value=value) + elif operation == "power": + self.pending = self.executor.submit(self._power_request, bool(enabled)) + elif operation == "auto_scan": + # Opening the page never starts discovery while Android Auto holds the adapter. + self.pending = self.executor.submit(self.owner.request, "scan", delegate=False) else: self.pending = self.executor.submit(self.owner.request, operation, address=address, enabled=enabled) self.operation = operation @@ -140,6 +146,9 @@ class BluetoothLarge(Widget): self.return_to_root = return_to_root self._rebuild() + def _power_request(self, enabled: bool) -> dict: + return self.owner.request("power", address=None, enabled=enabled) + def _forget(self, address: str) -> None: self._request("forget", address=address) @@ -190,7 +199,7 @@ class BluetoothLarge(Widget): self.status.get("powered") and self.status.get("parked")): self.scan_on_ready = False if not self.status.get("discovering"): - self._request("scan") + self._request("auto_scan") return if self.pending is None and time.monotonic() >= self.next_refresh: self._snapshot() @@ -239,7 +248,7 @@ class BluetoothLarge(Widget): def _rebuild(self): status = self.status or {} - devices = tuple((item.get("address"), item.get("name"), item.get("paired"), item.get("connected")) + devices = tuple((item.get("address"), item.get("name"), item.get("paired"), item.get("connected"), item.get("trusted")) for item in status.get("devices", ())) signature = repr((self.status is None, status.get("available"), status.get("powered"), status.get("parked"), status.get("discovering"), status.get("errorCode"), status.get("pairing"), devices, @@ -277,7 +286,11 @@ class BluetoothLarge(Widget): rows.append(button_item("Reject pairing", "Reject", callback=lambda p=prompt: self._request("pairing_response", prompt_id=p["id"], accepted=False), enabled=self._pairing_ready)) rows.append(button_item("Cancel pairing", "Cancel", callback=lambda: self._request("cancel_pair"), enabled=self._pairing_ready)) + links = [] for device in status.get("devices", ()): + if is_app_link(device): + links.append(device) + continue address = device.get("address") name = "".join(char for char in str(device.get("name") or "") if char.isprintable()).strip() address_name = bool(re.fullmatch(r"(?:[0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}", name)) @@ -290,6 +303,12 @@ class BluetoothLarge(Widget): rows.append(button_item("Forget " + device_label(device), "Forget", callback=lambda a=address: self._confirm("Forget this Bluetooth device?", lambda: self._forget(a)), enabled=lambda: self.active and self._ready())) + if links: + rows.append(text_item("App pairings", "Phones connected through the StarPilot app. These are not Bluetooth pairings.")) + for device in links: + link_address = device.get("address") + rows.append(button_item(device_label(device), "Forget", description=link_address, callback=lambda a=link_address: + self._confirm("Forget this app connection?", lambda: self._forget(a)), enabled=lambda: self.active and self._ready())) rows.extend(large_phone_rows(self.phone_pairing_allowed)) previous = getattr(self, "_scroller", None) self._scroller = Scroller(rows, spacing=0, line_separator=True) diff --git a/openpilot/starpilot/ui/settings.py b/openpilot/starpilot/ui/settings.py index 5b6c76eb1a..81e8f41e10 100644 --- a/openpilot/starpilot/ui/settings.py +++ b/openpilot/starpilot/ui/settings.py @@ -104,6 +104,9 @@ class SettingsView: rl.draw_texture_ex(background, rl.Vector2(x, top), 0, 1, rl.WHITE) font_size = 40 if destination == Destination.FORCE_DRIVE else 42 if destination == Destination.DRIVING_MODEL else ( 48 if destination in (Destination.GALAXY, Destination.PAIR) else 64) + if destination == Destination.DRIVING_CONTROLS: + font_size = 56 + text = text.replace(" ", "\n", 1) measured = self.fonts.measure(text, FontRole.BOLD, font_size) self.fonts.draw(text, FontRole.BOLD, font_size, x + 40, top + 180 - 23 - measured.height, rl.Color(255, 255, 255, 229)) diff --git a/openpilot/starpilot/ui/speed_limit_shapes.py b/openpilot/starpilot/ui/speed_limit_shapes.py index e8e1fbbaa4..9e01362141 100644 --- a/openpilot/starpilot/ui/speed_limit_shapes.py +++ b/openpilot/starpilot/ui/speed_limit_shapes.py @@ -14,8 +14,9 @@ SPRITES = { class SpeedLimitShapes: - def __init__(self, scale=1): + def __init__(self, scale=1, *, headless_context=False): self.scale = scale + self._headless_context = headless_context self.texture = None self.sources = {name: rl.Rectangle(*rect) for name, rect in SPRITES.items()} self.origins = {name: rl.Vector2(rect[2] * scale / 2, rect[3] * scale / 2) for name, rect in SPRITES.items()} @@ -23,7 +24,7 @@ class SpeedLimitShapes: def _load(self): if self.texture is not None: return - if not rl.is_window_ready(): + if not (rl.is_window_ready() or self._headless_context): raise RuntimeError('Speed-limit shapes require a graphics context') image = rl.load_image(str(ATLAS_PATH)) try: @@ -56,6 +57,6 @@ class SpeedLimitShapes: self._draw('arrow_large' if size == 'large' else 'arrow_compact', x, y, color, 180 if direction < 0 else 0) def close(self): - if self.texture is not None and rl.is_window_ready(): + if self.texture is not None and (rl.is_window_ready() or self._headless_context): rl.unload_texture(self.texture) self.texture = None diff --git a/openpilot/starpilot/ui/speed_limit_widget.py b/openpilot/starpilot/ui/speed_limit_widget.py index d16c6fd5c4..a4cc572cc7 100644 --- a/openpilot/starpilot/ui/speed_limit_widget.py +++ b/openpilot/starpilot/ui/speed_limit_widget.py @@ -62,7 +62,7 @@ class SpeedLimitWidget: self.scale = LARGE_SIGN_SCALE if profile == Profile.LARGE else 1 self.text_scale = self.scale * Profile.COMPACT.font_scale / profile.font_scale self.pulse = SpeedLimitPulse() - self.shapes = SpeedLimitShapes(self.scale) + self.shapes = SpeedLimitShapes(self.scale, headless_context=getattr(fonts, '_headless_context', False)) def close(self): self.shapes.close() diff --git a/openpilot/starpilot/ui/tests/test_bluetooth_compact.py b/openpilot/starpilot/ui/tests/test_bluetooth_compact.py index fda0a33b6d..631fd14154 100644 --- a/openpilot/starpilot/ui/tests/test_bluetooth_compact.py +++ b/openpilot/starpilot/ui/tests/test_bluetooth_compact.py @@ -23,18 +23,18 @@ class TestBluetoothCompact(unittest.TestCase): page._open_device("AA:BB:CC:DD:EE:FF") request.assert_called_once_with("pair", address="AA:BB:CC:DD:EE:FF") - def test_forget_falls_back_to_galaxy_when_android_auto_holds_the_adapter(self): - from openpilot.starpilot.bluetooth.owner import BluetoothRejected + def test_connected_unpaired_phone_app_is_forgotten_not_paired(self): page = BluetoothCompact.__new__(BluetoothCompact) - page.owner = NS(request=Mock(side_effect=BluetoothRejected("busy elsewhere", code="busy"))) # ty: ignore[invalid-assignment] # test double - call = Mock(return_value=(200, {"devices": []})) - with patch("openpilot.starpilot.galaxy_bluetooth.phone_ui.galaxy_call", return_value=call): - self.assertEqual(page._device_request("forget", "AA:BB:CC:DD:EE:FF"), {"devices": []}) - call.assert_called_once_with("POST", "/api/bluetooth/action", {"operation": "forget", "address": "AA:BB:CC:DD:EE:FF"}) - page.owner.request.side_effect = BluetoothRejected("changed") - with patch("openpilot.starpilot.galaxy_bluetooth.phone_ui.galaxy_call") as galaxy, self.assertRaises(BluetoothRejected): - page._device_request("forget", "AA:BB:CC:DD:EE:FF") - galaxy.assert_not_called() + page.icon = object() + page.status = {"devices": [{"address": "AA:BB:CC:DD:EE:FF", "name": "iPhone", "paired": False, "connected": True}]} + with patch.object(page, "_request") as request, \ + patch("openpilot.starpilot.ui.bluetooth_compact.gui_app.push_widget") as push, \ + patch("openpilot.starpilot.ui.bluetooth_compact.BigConfirmationDialog") as dialog: + page._open_device("AA:BB:CC:DD:EE:FF") + request.assert_not_called() + push.assert_called_once() + dialog.call_args.args[2]() + request.assert_called_once_with("forget", address="AA:BB:CC:DD:EE:FF", return_to_root=True) def pair_page(self, kind="confirmation", value="123456"): class Button: @@ -165,7 +165,7 @@ class TestBluetoothCompact(unittest.TestCase): page.return_to_root = False with patch.object(BluetoothCompact, "_rebuild") as rebuild: page._request("power", enabled=True) - page.executor.submit.assert_called_once_with(owner.request, "power", address=None, enabled=True) + page.executor.submit.assert_called_once_with(page._power_request, True) owner.request.assert_not_called() self.assertIs(page.pending, future) result = {"available": True, "parked": True, "powered": True, "devices": []} diff --git a/openpilot/starpilot/ui/tests/test_bluetooth_large_pure.py b/openpilot/starpilot/ui/tests/test_bluetooth_large_pure.py index 531e2a383c..0a82ef74ba 100644 --- a/openpilot/starpilot/ui/tests/test_bluetooth_large_pure.py +++ b/openpilot/starpilot/ui/tests/test_bluetooth_large_pure.py @@ -6,6 +6,7 @@ import re import time import unittest import uuid +from unittest.mock import Mock, patch class Widget: @@ -110,7 +111,8 @@ class TestBluetoothLarge(unittest.TestCase): 'text_item': lambda *a, **kw: Row((a, kw)), 'ConfirmDialog': Dialog, 'alert_dialog': Dialog, 'DialogResult': type('Result', (), {'CONFIRM': 1}), 'Keyboard': Dialog, 'setup_status': lambda: 'Ready to pair', 'large_phone_rows': lambda allowed: [], - 'forget_phone_keys': lambda allowed, address: self.forgotten.append(address)} + 'forget_phone_keys': lambda allowed, address: self.forgotten.append(address), + 'is_app_link': lambda device: bool(device.get('connected')) and not device.get('paired') and not device.get('trusted')} tree.body.insert(0, ast.ImportFrom(module='__future__', names=[ast.alias(name='annotations')], level=0)) exec(compile(ast.fix_missing_locations(tree), str(path), 'exec'), scope) self.panel_type = scope['BluetoothLarge'] @@ -129,11 +131,30 @@ class TestBluetoothLarge(unittest.TestCase): self.assertEqual(self.panel.executor.jobs[0][2], ()) self.assertTrue(self.panel.owner.session_valid(self.panel.session)) + def test_large_device_buttons_reach_galaxy_when_projection_owns_bluetooth(self): + from openpilot.starpilot.bluetooth.native_owner import NativeBluetoothOwner + from openpilot.starpilot.bluetooth.owner import AdapterBusyElsewhere, BluetoothOwner + self.panel.owner = NativeBluetoothOwner(lambda: True) + self.panel.active = True + self.panel.session = ('large', 'test') + self.panel.executor = Executor() + call = Mock(return_value=(200, {'devices': [], 'available': True, 'powered': True})) + with patch.object(BluetoothOwner, 'request', side_effect=AdapterBusyElsewhere('AA holds adapter')), \ + patch('openpilot.starpilot.galaxy_bluetooth.phone_ui.galaxy_call', return_value=call): + for operation in ('scan', 'pair', 'connect', 'disconnect', 'forget'): + address = None if operation == 'scan' else 'AA:BB:CC:DD:EE:FF' + self.panel._submit_request(operation, address, None, False, self.panel.session, None, None, '') + future, callback, args, kwargs = self.panel.executor.jobs[-1] + future.set_result(callback(*args, **kwargs)) + payload = {'operation': operation, **({'address': address} if address else {})} + call.assert_called_with('POST', '/api/bluetooth/action', payload) + def test_open_powered_panel_scans_once_and_stops_on_hide(self): self.panel.show_event() self.panel.pending.set_result({'available': True, 'powered': True, 'parked': True, 'discovering': False}) self.panel._tick() - self.assertEqual(self.panel.operation, 'scan') + self.assertEqual(self.panel.operation, 'auto_scan') + self.assertEqual(self.panel.executor.jobs[-1][3], {'delegate': False}) self.panel.pending.set_result({'available': True, 'powered': True, 'parked': True, 'discovering': False}) self.panel._tick() self.assertEqual(sum(job[2] == ('scan',) for job in self.panel.executor.jobs), 1) @@ -141,6 +162,18 @@ class TestBluetoothLarge(unittest.TestCase): self.panel.hide_event() self.assertEqual(executor.jobs[-1][1], self.panel.owner.close) + def test_opening_the_page_never_scans_through_android_auto(self): + from openpilot.starpilot.bluetooth.native_owner import NativeBluetoothOwner + from openpilot.starpilot.bluetooth.owner import AdapterBusyElsewhere, BluetoothOwner + owner = NativeBluetoothOwner(lambda: True) + local = {'available': True, 'powered': True, 'parked': True, 'discovering': False, 'devices': []} + with patch.object(BluetoothOwner, 'request', side_effect=AdapterBusyElsewhere('AA holds adapter')), \ + patch.object(BluetoothOwner, 'snapshot', return_value=local), \ + patch('openpilot.starpilot.galaxy_bluetooth.phone_ui.galaxy_call') as factory: + self.assertEqual(owner.request('scan', delegate=False), local) + factory.assert_not_called() + self.assertIsNone(owner.galaxy) + def test_pairing_prompt_opens_once_and_expired_prompt_is_retired(self): prompt = {'id': 'a' * 32, 'kind': 'confirmation', 'value': '123456', 'displayOnly': False} status: dict[str, Any] = {'available': True, 'powered': True, 'parked': True, 'discovering': False, @@ -182,6 +215,19 @@ class TestBluetoothLarge(unittest.TestCase): self.assertIn(('Headphones', 'Disconnect'), [row[0] for row in rows]) self.assertIn(('Forget Headphones', 'Forget'), [row[0] for row in rows]) + def test_app_connections_are_grouped_apart_from_bluetooth_devices(self): + self.panel.status = {'available': True, 'powered': True, 'parked': True, 'devices': [ + {'address': 'AA:BB', 'name': 'Headphones', 'paired': True, 'connected': True}, + {'address': 'CC:DD', 'name': 'iPhone', 'paired': False, 'trusted': False, 'connected': True}]} + self.panel._scroller = None + self.panel._rebuild() + names = [row[0] for row in self.panel._scroller.rows] + self.assertIn(('Headphones', 'Disconnect'), names) + self.assertNotIn(('iPhone', 'Pair'), names) + self.assertNotIn(('iPhone', 'Disconnect'), names) + self.assertIn('App pairings', [name[0] for name in names]) + self.assertIn(('iPhone', 'Forget'), names) + def test_forgetting_a_device_also_forgets_its_app_key(self): self.panel.show_event() self.panel.status = {'available': True, 'powered': True, 'parked': True} diff --git a/openpilot/starpilot/ui/tests/test_speed_limit_shapes.py b/openpilot/starpilot/ui/tests/test_speed_limit_shapes.py index c524d5cac5..360cb38e63 100644 --- a/openpilot/starpilot/ui/tests/test_speed_limit_shapes.py +++ b/openpilot/starpilot/ui/tests/test_speed_limit_shapes.py @@ -83,3 +83,12 @@ def test_texture_setup_failure_releases_both_resources(gpu): shapes.SpeedLimitShapes().circle(0, 0, shapes.rl.RED) gpu['unload_texture'].assert_called_once() gpu['unload_image'].assert_called_once() + + +def test_headless_context_loads_atlas_without_a_window(gpu, monkeypatch): + monkeypatch.setattr(shapes.rl, 'is_window_ready', lambda: False) + with pytest.raises(RuntimeError, match='graphics context'): + shapes.SpeedLimitShapes().circle(0, 0, shapes.rl.RED) + owner = shapes.SpeedLimitShapes(headless_context=True) + owner.circle(0, 0, shapes.rl.RED) + gpu['load_texture_from_image'].assert_called_once()