Source code for bauiv1lib.localnetwork

# Released under the MIT License. See LICENSE for details.
#
"""UI for the local-network permission (Android 17+).

Android 17 requires a runtime permission for any local-network traffic
(LAN scanning, hosting, joining by address). Everywhere else the
permission always reads as granted, so none of this UI ever shows.
"""

from __future__ import annotations  # Docs-generation hack.

from enum import Enum
from typing import TYPE_CHECKING, assert_never

import bauiv1 as bui
from bauiv1 import _classicassets, _commonassets

if TYPE_CHECKING:
    from typing import Callable

# How often UI showing permission state re-checks it. The check reads a
# cached native value (Android pushes changes to it), so it's cheap.
UPDATE_INTERVAL = 1.0

WARNING_COLOR = (1.0, 0.85, 0.2)

# Leads warning text. (Two spaces; the emoji's advance renders narrow,
# so one looks cramped.)
WARNING_PREFIX = '⚠️  '

# Vertical room UI should leave for a LocalNetworkPermissionWarning.
WARNING_HEIGHT = 70.0


[docs] def local_network_permission_missing() -> bool: """Whether we currently lack local-network permission.""" return ( get_local_network_permission_state() is not LocalNetworkPermissionState.GRANTED )
[docs] class LocalNetworkPermissionState(Enum): """Where we stand on the local-network permission.""" GRANTED = 'granted' #: Not granted; requesting it will show a system prompt. REQUESTABLE = 'requestable' #: Denied for good; only system settings can grant it now. DENIED = 'denied'
[docs] def get_local_network_permission_state() -> LocalNetworkPermissionState: """Return our current local-network permission state.""" if bui.have_permission(bui.Permission.LOCAL_NETWORK): return LocalNetworkPermissionState.GRANTED if bui.can_request_permission(bui.Permission.LOCAL_NETWORK): return LocalNetworkPermissionState.REQUESTABLE return LocalNetworkPermissionState.DENIED
[docs] def request_local_network_permission() -> None: """Ask the OS for the local-network permission.""" bui.request_permission(bui.Permission.LOCAL_NETWORK)
[docs] def show_local_network_permission_dialog( *, origin_widget: bui.Widget | None, on_continue: Callable[[], None], ) -> None: """Explain that local network play needs permission. Offers to request it (then just closes; whatever UI is showing picks up the change) or to continue without it via ``on_continue``. If the permission can no longer be requested, it explains how to enable it in settings and then continues. """ # pylint: disable=cyclic-import from bauiv1lib.confirm import ConfirmWindow state = get_local_network_permission_state() if state is LocalNetworkPermissionState.GRANTED: on_continue() return if state is LocalNetworkPermissionState.REQUESTABLE: ConfirmWindow( text=_classicassets.strings.gather.local_network_permission_prompt, # Request goes on the left (ConfirmWindow's cancel slot) and # starts selected; 'Not Now' on the right carries on. cancel_action=request_local_network_permission, action=on_continue, cancel_is_selected=True, cancel_text=( _classicassets.strings.gather.request_local_network_permission ), ok_text=_commonassets.strings.actions.not_now, width=500, height=150, origin_widget=origin_widget, ) return if state is LocalNetworkPermissionState.DENIED: ConfirmWindow( text=_settings_instructions(), action=on_continue, cancel_button=False, width=500, height=150, origin_widget=origin_widget, ) return assert_never(state)
def _settings_instructions() -> bui.LangStr: # (Wraps to two lines at display; set in the string's workspace.) return _classicassets.strings.gather.local_network_settings_instructions( app_name=bui.appnameupper() ) def _warning_text( text: bui.LangStr, *, lines: int | None = None ) -> bui.LangStr: """Prefix text with our warning icon. Joining doesn't carry the inner string's line-wrapping through to the result, so pass ``lines`` to re-wrap the whole thing to that many lines. """ joined = bui.LangStr.join([bui.LangStr.from_text(WARNING_PREFIX), text]) if lines is not None: return bui.LangStr(joined.to_json(), wrap=(lines, lines, None)) return joined
[docs] class LocalNetworkPermissionWarning: """Warning text (plus a request button if possible) shown in UI that needs local network access while we don't have it. Re-checks the permission every ``UPDATE_INTERVAL`` seconds and updates (or clears) itself to match. """ def __init__( self, *, parent: bui.Widget, position: tuple[float, float], width: float, idprefix: str, ): self._parent = parent self._position = position self._width = width self._idprefix = idprefix self._state: LocalNetworkPermissionState | None = None self._text: bui.Widget | None = None self._button: bui.Widget | None = None self._update_timer = bui.AppTimer( UPDATE_INTERVAL, bui.WeakCallStrict(self._update), repeat=True ) self._update() def _clear(self) -> None: for widget in (self._text, self._button): if widget: widget.delete() self._text = self._button = None def _update(self) -> None: if not self._parent: return state = get_local_network_permission_state() if state is self._state: return self._state = state self._clear() x, y = self._position gstrs = _classicassets.strings.gather if state is LocalNetworkPermissionState.GRANTED: return if state is LocalNetworkPermissionState.REQUESTABLE: button_width = 240.0 text_width = self._width - button_width - 20.0 self._text = bui.textwidget( parent=self._parent, position=(x - self._width * 0.5 + text_width * 0.5, y), size=(0, 0), h_align='center', v_align='center', color=WARNING_COLOR, scale=0.9, maxwidth=text_width, text=_warning_text(gstrs.local_network_permission_needed), ) self._button = bui.buttonwidget( parent=self._parent, id=f'{self._idprefix}|requestlocalnetwork', position=( x + self._width * 0.5 - button_width, y - 20.0, ), size=(button_width, 40.0), autoselect=True, color=(0.6, 0.53, 0.2), textcolor=(1.0, 1.0, 0.8), label=gstrs.request_local_network_permission, on_activate_call=request_local_network_permission, ) return if state is LocalNetworkPermissionState.DENIED: self._text = bui.textwidget( parent=self._parent, position=(x, y), size=(0, 0), h_align='center', v_align='center', color=WARNING_COLOR, scale=0.8, maxwidth=self._width, text=_warning_text(_settings_instructions(), lines=2), ) return assert_never(state)
# Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self, TypeIs) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum