From eb209ed149b2fb7de642ef039524668346f7e80e Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 2 Aug 2023 14:32:18 -0700 Subject: [PATCH] Android support (#1666) * Added `page.debug` and `page.platform_brightness` props Close #1649, close #1630 * Store session ID in `window.name` Fix #1629 * Do not assign random port on Windows with `--ios` flag Fix #1620 * Update local_connection.py * Build Android QR --- client/android/build.gradle | 2 +- package/lib/src/actions.dart | 3 +- package/lib/src/flet_server.dart | 8 +++ .../protocol/register_webclient_request.dart | 6 ++ package/lib/src/reducers.dart | 30 ++++++++- .../lib/src/utils/session_store_non_web.dart | 8 +++ package/lib/src/utils/session_store_web.dart | 8 +++ package/lib/src/widgets/page_media.dart | 3 +- .../register_webclient_request_test.dart | 2 +- .../src/flet_core/local_connection.py | 6 ++ .../packages/flet-core/src/flet_core/page.py | 54 +++++++++++----- .../flet-core/src/flet_core/protocol.py | 2 + .../flet/src/flet/cli/commands/run.py | 61 +++++++++++++++---- server/page/client.go | 2 +- server/page/payloads.go | 26 ++++---- server/page/session_handler.go | 4 +- 16 files changed, 178 insertions(+), 47 deletions(-) diff --git a/client/android/build.gradle b/client/android/build.gradle index 83ae22004..3cdaac958 100644 --- a/client/android/build.gradle +++ b/client/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/package/lib/src/actions.dart b/package/lib/src/actions.dart index 995cfdc1f..497528b18 100644 --- a/package/lib/src/actions.dart +++ b/package/lib/src/actions.dart @@ -50,7 +50,8 @@ class WindowEventAction { class PageBrightnessChangeAction { final Brightness brightness; - PageBrightnessChangeAction(this.brightness); + final FletServer server; + PageBrightnessChangeAction(this.brightness, this.server); } class RegisterWebClientAction { diff --git a/package/lib/src/flet_server.dart b/package/lib/src/flet_server.dart index 58fc32600..da4e166d4 100644 --- a/package/lib/src/flet_server.dart +++ b/package/lib/src/flet_server.dart @@ -44,7 +44,9 @@ class FletServer { String _windowLeft = ""; String _isPWA = ""; String _isWeb = ""; + String _isDebug = ""; String _platform = ""; + String _platformBrightness = ""; int reconnectStarted = 0; final Map controlInvokeMethods; @@ -114,7 +116,9 @@ class FletServer { required String windowLeft, required String isPWA, required String isWeb, + required String isDebug, required String platform, + required String platformBrightness, }) { _pageName = pageName; _pageHash = pageRoute; @@ -126,7 +130,9 @@ class FletServer { _windowLeft = windowLeft; _isPWA = isPWA; _isWeb = isWeb; + _isDebug = isDebug; _platform = platform; + _platformBrightness = platformBrightness; } registerWebClientInternal() { @@ -145,7 +151,9 @@ class FletServer { windowHeight: page?.attrString("windowHeight") ?? _windowHeight, isPWA: _isPWA, isWeb: _isWeb, + isDebug: _isDebug, platform: _platform, + platformBrightness: _platformBrightness, sessionId: _store.state.sessionId))); _pageHash = ""; } diff --git a/package/lib/src/protocol/register_webclient_request.dart b/package/lib/src/protocol/register_webclient_request.dart index 148107e0a..b55de0220 100644 --- a/package/lib/src/protocol/register_webclient_request.dart +++ b/package/lib/src/protocol/register_webclient_request.dart @@ -9,7 +9,9 @@ class RegisterWebClientRequest { final String? windowLeft; final String? isPWA; final String? isWeb; + final String? isDebug; final String? platform; + final String? platformBrightness; final String? sessionId; RegisterWebClientRequest( @@ -23,7 +25,9 @@ class RegisterWebClientRequest { this.windowLeft, this.isPWA, this.isWeb, + this.isDebug, this.platform, + this.platformBrightness, this.sessionId}); Map toJson() => { @@ -37,7 +41,9 @@ class RegisterWebClientRequest { 'windowLeft': windowLeft, 'isPWA': isPWA, 'isWeb': isWeb, + 'isDebug': isDebug, 'platform': platform, + 'platformBrightness': platformBrightness, 'sessionId': sessionId }; } diff --git a/package/lib/src/reducers.dart b/package/lib/src/reducers.dart index c20ab9427..0e14b2069 100644 --- a/package/lib/src/reducers.dart +++ b/package/lib/src/reducers.dart @@ -27,7 +27,7 @@ enum Actions { increment, setText, setError } AppState appReducer(AppState state, dynamic action) { if (action is PageLoadAction) { - var sessionId = SessionStore.get("sessionId"); + var sessionId = SessionStore.sessionId; return state.copyWith( pageUri: action.pageUri, assetsDir: action.assetsDir, @@ -95,7 +95,9 @@ AppState appReducer(AppState state, dynamic action) { windowLeft: wmd.left != null ? wmd.left.toString() : "", isPWA: isProgressiveWebApp().toString(), isWeb: kIsWeb.toString(), - platform: defaultTargetPlatform.name.toLowerCase()); + isDebug: kDebugMode.toString(), + platform: defaultTargetPlatform.name.toLowerCase(), + platformBrightness: state.displayBrightness.name.toString()); action.server.connect(address: state.pageUri!.toString()); }); @@ -137,6 +139,28 @@ AppState appReducer(AppState state, dynamic action) { return state.copyWith(controls: controls); } else if (action is PageBrightnessChangeAction) { + // + // platform brightness changed + // + debugPrint("New platform brightness: ${action.brightness.name}"); + + var page = state.controls["page"]; + var controls = Map.of(state.controls); + if (page != null && !state.isLoading) { + var pageAttrs = Map.of(page.attrs); + pageAttrs["platformBrightness"] = action.brightness.name.toString(); + + List> props = [ + {"i": "page", "platformBrightness": action.brightness.name.toString()}, + ]; + + controls[page.id] = page.copyWith(attrs: pageAttrs); + action.server.updateControlProps(props: props); + action.server.sendPageEvent( + eventTarget: "page", + eventName: "platformBrightnessChange", + eventData: action.brightness.name.toString()); + } return state.copyWith(displayBrightness: action.brightness); } else if (action is RegisterWebClientAction) { // @@ -152,7 +176,7 @@ AppState appReducer(AppState state, dynamic action) { final sessionId = action.payload.session!.id; // store sessionId in a cookie - SessionStore.set("sessionId", sessionId); + SessionStore.sessionId = sessionId; if (state.deepLinkingRoute != "") { debugPrint( diff --git a/package/lib/src/utils/session_store_non_web.dart b/package/lib/src/utils/session_store_non_web.dart index 2f47709cf..ffbe51e34 100644 --- a/package/lib/src/utils/session_store_non_web.dart +++ b/package/lib/src/utils/session_store_non_web.dart @@ -1,6 +1,14 @@ import 'package:flutter/foundation.dart'; class SessionStore { + static String? get sessionId { + return null; + } + + static set sessionId(String? value) { + // nothing to do + } + static String? get(String name) { return null; } diff --git a/package/lib/src/utils/session_store_web.dart b/package/lib/src/utils/session_store_web.dart index 65428b356..fad3d0c02 100644 --- a/package/lib/src/utils/session_store_web.dart +++ b/package/lib/src/utils/session_store_web.dart @@ -4,6 +4,14 @@ import 'dart:html' as html; import 'package:flutter/foundation.dart'; class SessionStore { + static String? get sessionId { + return html.window.name; + } + + static set sessionId(String? value) { + html.window.name = value; + } + static String? get(String name) { debugPrint("Get session storage $name"); diff --git a/package/lib/src/widgets/page_media.dart b/package/lib/src/widgets/page_media.dart index 0d6d4e805..78fb3c2bb 100644 --- a/package/lib/src/widgets/page_media.dart +++ b/package/lib/src/widgets/page_media.dart @@ -43,7 +43,8 @@ class _PageMediaState extends State { _onScreenBrightnessChanged(Brightness brightness, Function dispatch) { debugPrint("Send new brightness to reducer: $brightness"); - dispatch(PageBrightnessChangeAction(brightness)); + dispatch(PageBrightnessChangeAction( + brightness, FletAppServices.of(context).server)); } @override diff --git a/package/test/protocol/register_webclient_request_test.dart b/package/test/protocol/register_webclient_request_test.dart index 7d80e4d36..aad415d7c 100644 --- a/package/test/protocol/register_webclient_request_test.dart +++ b/package/test/protocol/register_webclient_request_test.dart @@ -12,6 +12,6 @@ void main() { final j = json.encode(m); expect(j, - '{"action":"registerWebClient","payload":{"pageName":"test-page1","pageRoute":null,"pageWidth":null,"pageHeight":null,"windowWidth":null,"windowHeight":null,"windowTop":null,"windowLeft":null,"isPWA":null,"isWeb":null,"platform":null,"sessionId":null}}'); + '{"action":"registerWebClient","payload":{"pageName":"test-page1","pageRoute":null,"pageWidth":null,"pageHeight":null,"windowWidth":null,"windowHeight":null,"windowTop":null,"windowLeft":null,"isPWA":null,"isWeb":null,"isDebug":null,"platform":null,"platformBrightness":null,"sessionId":null}}'); }); } diff --git a/sdk/python/packages/flet-core/src/flet_core/local_connection.py b/sdk/python/packages/flet-core/src/flet_core/local_connection.py index e1ef43833..6b7a59796 100644 --- a/sdk/python/packages/flet-core/src/flet_core/local_connection.py +++ b/sdk/python/packages/flet-core/src/flet_core/local_connection.py @@ -36,7 +36,9 @@ def _create_register_web_client_response(self): "windowleft": self._client_details.windowLeft, "pwa": self._client_details.isPWA, "web": self._client_details.isWeb, + "debug": self._client_details.isDebug, "platform": self._client_details.platform, + "platformBrightness": self._client_details.platformBrightness, } }, ), @@ -216,8 +218,12 @@ def _process_get_command(self, values: List[str]): r = self._client_details.isPWA elif prop_name == "web": r = self._client_details.isWeb + elif prop_name == "debug": + r = self._client_details.isDebug elif prop_name == "platform": r = self._client_details.platform + elif prop_name == "platformBrightness": + r = self._client_details.platformBrightness elif prop_name == "width": r = self._client_details.pageWidth elif prop_name == "height": diff --git a/sdk/python/packages/flet-core/src/flet_core/page.py b/sdk/python/packages/flet-core/src/flet_core/page.py index 79a9c19bd..9e0b682e7 100644 --- a/sdk/python/packages/flet-core/src/flet_core/page.py +++ b/sdk/python/packages/flet-core/src/flet_core/page.py @@ -124,6 +124,11 @@ def __init__(self, conn: Connection, session_id): self._add_event_handler("close", self.__on_close.get_handler()) self.__on_resize = EventHandler() self._add_event_handler("resize", self.__on_resize.get_handler()) + self.__on_platform_brightness_change = EventHandler() + self._add_event_handler( + "platformBrightnessChange", + self.__on_platform_brightness_change.get_handler(), + ) self.__last_route = None @@ -220,13 +225,11 @@ async def fetch_page_details_async(self): def __get_page_detail_commands(self): return [ Command(0, "get", ["page", "route"]), - Command( - 0, - "get", - ["page", "pwa"], - ), + Command(0, "get", ["page", "pwa"]), Command(0, "get", ["page", "web"]), + Command(0, "get", ["page", "debug"]), Command(0, "get", ["page", "platform"]), + Command(0, "get", ["page", "platformBrightness"]), Command(0, "get", ["page", "width"]), Command(0, "get", ["page", "height"]), Command(0, "get", ["page", "windowWidth"]), @@ -241,15 +244,17 @@ def __set_page_details(self, values): self._set_attr("route", values[0], False) self._set_attr("pwa", values[1], False) self._set_attr("web", values[2], False) - self._set_attr("platform", values[3], False) - self._set_attr("width", values[4], False) - self._set_attr("height", values[5], False) - self._set_attr("windowWidth", values[6], False) - self._set_attr("windowHeight", values[7], False) - self._set_attr("windowTop", values[8], False) - self._set_attr("windowLeft", values[9], False) - self._set_attr("clientIP", values[10], False) - self._set_attr("clientUserAgent", values[11], False) + self._set_attr("debug", values[3], False) + self._set_attr("platform", values[4], False) + self._set_attr("platformBrightness", values[5], False) + self._set_attr("width", values[6], False) + self._set_attr("height", values[7], False) + self._set_attr("windowWidth", values[8], False) + self._set_attr("windowHeight", values[9], False) + self._set_attr("windowTop", values[10], False) + self._set_attr("windowLeft", values[11], False) + self._set_attr("clientIP", values[12], False) + self._set_attr("clientUserAgent", values[13], False) def update(self, *controls): with self.__lock: @@ -1117,11 +1122,23 @@ def pwa(self): def web(self) -> bool: return cast(bool, self._get_attr("web", data_type="bool", def_value=False)) + # debug + @property + def debug(self) -> bool: + return cast(bool, self._get_attr("debug", data_type="bool", def_value=False)) + # platform @property def platform(self): return self._get_attr("platform") + # platform_brightness + @property + def platform_brightness(self) -> ThemeMode: + brightness = self._get_attr("platformBrightness") + assert brightness is not None + return ThemeMode(brightness) + # client_ip @property def client_ip(self): @@ -1659,6 +1676,15 @@ def on_resize(self): def on_resize(self, handler): self.__on_resize.subscribe(handler) + # on_platform_brightness_change + @property + def on_platform_brightness_change(self): + return self.__on_platform_brightness_change + + @on_platform_brightness_change.setter + def on_platform_brightness_change(self, handler): + self.__on_platform_brightness_change.subscribe(handler) + # on_route_change @property def on_route_change(self): diff --git a/sdk/python/packages/flet-core/src/flet_core/protocol.py b/sdk/python/packages/flet-core/src/flet_core/protocol.py index e7c649641..57ca6d713 100644 --- a/sdk/python/packages/flet-core/src/flet_core/protocol.py +++ b/sdk/python/packages/flet-core/src/flet_core/protocol.py @@ -147,7 +147,9 @@ class RegisterWebClientRequestPayload: windowLeft: str isPWA: str isWeb: str + isDebug: str platform: str + platformBrightness: str sessionId: str diff --git a/sdk/python/packages/flet/src/flet/cli/commands/run.py b/sdk/python/packages/flet/src/flet/cli/commands/run.py index 6990d4845..69c1aea12 100644 --- a/sdk/python/packages/flet/src/flet/cli/commands/run.py +++ b/sdk/python/packages/flet/src/flet/cli/commands/run.py @@ -95,6 +95,13 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: default=False, help="open app on iOS device", ) + parser.add_argument( + "--android", + dest="android", + action="store_true", + default=False, + help="open app on Android device", + ) parser.add_argument( "-a", "--assets", @@ -125,10 +132,10 @@ def handle(self, options: argparse.Namespace) -> None: script_dir = os.path.dirname(script_path) port = options.port - if port is None and (is_windows() or options.web): - port = get_free_tcp_port() - elif port is None and options.ios: + if port is None and (options.ios or options.android): port = 8551 + elif port is None and (is_windows() or options.web): + port = get_free_tcp_port() uds_path = None if port is None and not is_windows(): @@ -150,6 +157,7 @@ def handle(self, options: argparse.Namespace) -> None: uds_path, options.web, options.ios, + options.android, options.hidden, assets_dir, ) @@ -172,7 +180,17 @@ def handle(self, options: argparse.Namespace) -> None: class Handler(FileSystemEventHandler): def __init__( - self, args, script_path, port, page_name, uds_path, web, ios, hidden, assets_dir + self, + args, + script_path, + port, + page_name, + uds_path, + web, + ios, + android, + hidden, + assets_dir, ) -> None: super().__init__() self.args = args @@ -182,6 +200,7 @@ def __init__( self.uds_path = uds_path self.web = web self.ios = ios + self.android = android self.hidden = hidden self.assets_dir = assets_dir self.last_time = time.time() @@ -195,12 +214,12 @@ def __init__( def start_process(self): p_env = {**os.environ} - if self.web or self.ios: + if self.web or self.ios or self.android: p_env["FLET_FORCE_WEB_VIEW"] = "true" p_env["FLET_DETACH_FLETD"] = "true" # force page name for ios - if self.ios: + if self.ios or self.android: p_env["FLET_PAGE_NAME"] = "/".join(Path(self.script_path).parts[-2:]) if self.port is not None: p_env["FLET_SERVER_PORT"] = str(self.port) @@ -240,10 +259,14 @@ def print_output(self, p): if line.startswith(self.page_url_prefix): if not self.page_url: self.page_url = line[len(self.page_url_prefix) + 1 :] - if self.page_url.startswith("http") and not self.ios: + if ( + self.page_url.startswith("http") + and not self.ios + and not self.android + ): print(self.page_url) - if self.ios: - self.print_qr_code(self.page_url) + if self.ios or self.android: + self.print_qr_code(self.page_url, self.android) elif self.web: open_in_browser(self.page_url) else: @@ -268,7 +291,7 @@ def restart_program(self): self.p.wait() self.start_process() - def print_qr_code(self, orig_url: str): + def print_qr_code(self, orig_url: str, android: bool): u = urlparse(orig_url) hostname = socket.gethostname() ip_addr = socket.gethostbyname(hostname) @@ -278,9 +301,23 @@ def print_qr_code(self, orig_url: str): # self.clear_console() print("App is running on:", lan_url) print("") - qr_url = urlunparse( - ("flet", "flet-host", quote(lan_url, safe=""), None, None, None) + qr_url = ( + urlunparse( + ( + "https", + "android.flet.dev", + quote(f"{ip_addr}:{u.port}{u.path}", safe="/"), + None, + None, + None, + ) + ) + if android + else urlunparse( + ("flet", "flet-host", quote(lan_url, safe=""), None, None, None) + ) ) + # print(qr_url) qr = qrcode.QRCode() qr.add_data(qr_url) qr.print_ascii(invert=True) diff --git a/server/page/client.go b/server/page/client.go index fedf785a7..79738de12 100644 --- a/server/page/client.go +++ b/server/page/client.go @@ -270,7 +270,7 @@ func (c *Client) registerWebClientCore(request *RegisterWebClientRequestPayload) session = newSession(page, uuid.New().String(), c.clientIP, c.clientUserAgent, request.PageRoute, request.PageWidth, request.PageHeight, request.WindowWidth, request.WindowHeight, request.WindowTop, request.WindowLeft, - request.IsPWA, request.IsWeb, request.Platform) + request.IsPWA, request.IsWeb, request.IsDebug, request.Platform, request.PlatformBrightness) sessionCreated = true } else { log.Debugf("Existing session %s found for %s page\n", session.ID, page.Name) diff --git a/server/page/payloads.go b/server/page/payloads.go index 97656af5d..5f830b691 100644 --- a/server/page/payloads.go +++ b/server/page/payloads.go @@ -47,18 +47,20 @@ type RegisterHostClientResponsePayload struct { } type RegisterWebClientRequestPayload struct { - PageName string `json:"pageName"` - PageRoute string `json:"pageRoute"` - PageWidth string `json:"pageWidth"` - PageHeight string `json:"pageHeight"` - WindowWidth string `json:"windowWidth"` - WindowHeight string `json:"windowHeight"` - WindowTop string `json:"windowTop"` - WindowLeft string `json:"windowLeft"` - IsPWA string `json:"isPWA"` - Platform string `json:"platform"` - IsWeb string `json:"isWeb"` - SessionID string `json:"sessionID"` + PageName string `json:"pageName"` + PageRoute string `json:"pageRoute"` + PageWidth string `json:"pageWidth"` + PageHeight string `json:"pageHeight"` + WindowWidth string `json:"windowWidth"` + WindowHeight string `json:"windowHeight"` + WindowTop string `json:"windowTop"` + WindowLeft string `json:"windowLeft"` + IsPWA string `json:"isPWA"` + IsWeb string `json:"isWeb"` + IsDebug string `json:"isDebug"` + Platform string `json:"platform"` + PlatformBrightness string `json:"platformBrightness"` + SessionID string `json:"sessionID"` } type RegisterWebClientResponsePayload struct { diff --git a/server/page/session_handler.go b/server/page/session_handler.go index e29dfb286..8de93bb72 100644 --- a/server/page/session_handler.go +++ b/server/page/session_handler.go @@ -50,7 +50,7 @@ type addCommandBatchItem struct { func newSession(page *model.Page, id string, clientIP string, clientUserAgent string, pageRoute string, pageWidth string, pageHeight string, windowWidth string, windowHeight string, - windowTop string, windowLeft string, isPwa string, isWeb string, platform string) *model.Session { + windowTop string, windowLeft string, isPwa string, isWeb string, isDebug string, platform string, platformBrightness string) *model.Session { s := &model.Session{} s.Page = page s.ID = id @@ -69,7 +69,9 @@ func newSession(page *model.Page, id string, clientIP string, clientUserAgent st p.SetAttr("windowleft", windowLeft) p.SetAttr("pwa", isPwa) p.SetAttr("web", isWeb) + p.SetAttr("debug", isDebug) p.SetAttr("platform", platform) + p.SetAttr("platformBrightness", platformBrightness) p.SetAttr("clientIP", clientIP) p.SetAttr("clientUserAgent", clientUserAgent) h.addControl(p)