From 31a844bb944df3a143b8fa646e806e354f9f33e2 Mon Sep 17 00:00:00 2001 From: Mikkeli Date: Thu, 15 Oct 2026 22:33:43 +0900 Subject: [PATCH] Auto dark/bright follows the sun at the tablet's own location MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without a light sensor an app may read (the Redmi Pad SE has none), auto is dark while the sun is more than 4° below the horizon (lib/theme/sun.dart), at Android's coarse location, polled and kept across restarts. The tablet's own, not the Orin's GNSS: the tablet has its own GNSS and cellular. Android's dark theme is the last fallback. Co-Authored-By: Claude Opus 5.5 --- android/app/src/main/AndroidManifest.xml | 2 + .../kotlin/dev/yukart/display/MainActivity.kt | 34 ++++++++++++ docs/impl/plans/DASH-NEXT.md | 13 +++-- lib/README.md | 2 +- lib/main.dart | 21 ++++---- lib/pages/settings.dart | 6 +-- lib/theme/palette.dart | 4 +- lib/theme/sun.dart | 48 +++++++++++++++++ test/palette_test.dart | 53 +++++++++++++++++++ 9 files changed, 164 insertions(+), 19 deletions(-) create mode 100644 lib/theme/sun.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6c64c7d..ec92c7a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,8 @@ + + [@]` * runs the link test (lib/linktest.dart) without touching the screen. @@ -39,6 +44,7 @@ class MainActivity : FlutterActivity() { private var ethernet: Network? = null private var wantBinding = false private var pendingLinkTest: String? = null + private var askedLocation = false private val callback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { @@ -90,10 +96,38 @@ class MainActivity : FlutterActivity() { .build() cm.requestNetwork(request, callback) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "yukart/location").setMethodCallHandler { call, result -> + if (call.method == "last") result.success(lastLocation()) else result.notImplemented() + } + EventChannel(flutterEngine.dartExecutor.binaryMessenger, "yukart/light") .setStreamHandler(LightStream(getSystemService(Context.SENSOR_SERVICE) as SensorManager)) } + /** + * [lat, lon] of the newest location Android has (other apps' fixes included), or null. + * Asks for coarse location the first time; with none known, asks for one, which the + * next call sees. + */ + private fun lastLocation(): List? { + if (checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + if (!askedLocation) { + askedLocation = true + requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), 1) + } + return null + } + val lm = getSystemService(Context.LOCATION_SERVICE) as LocationManager + val enabled = lm.getProviders(true) + val best = enabled.mapNotNull { runCatching { lm.getLastKnownLocation(it) }.getOrNull() }.maxByOrNull { it.time } + if (best == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + listOf("fused", LocationManager.NETWORK_PROVIDER, LocationManager.GPS_PROVIDER) + .firstOrNull { it in enabled } + ?.let { runCatching { lm.getCurrentLocation(it, null, mainExecutor) {} } } + } + return best?.let { listOf(it.latitude, it.longitude) } + } + /** Lux from the default light sensor while listened to; an error at once without one, or when refused. */ private class LightStream(private val sm: SensorManager) : EventChannel.StreamHandler { private var listener: SensorEventListener? = null diff --git a/docs/impl/plans/DASH-NEXT.md b/docs/impl/plans/DASH-NEXT.md index 15f8080..5150f75 100644 --- a/docs/impl/plans/DASH-NEXT.md +++ b/docs/impl/plans/DASH-NEXT.md @@ -81,10 +81,15 @@ where the device lets an app read one (`theme/light.dart`: dark at ≤ 15 lux, b held 5 s; Settings shows the reading). **The Redmi Pad SE does not:** its only light sensor, `camera_light_Sensor`, is derived by Xiaomi from the front camera, and `registerListener` refuses apps at every rate (only the system's auto-brightness reads it). Its automatic brightness is not -exposed either (`screen_brightness` is the manual level). Other lux sources, if wanted: -the Orin's camera (its exposure and gain say how bright the road is; svc-vision would publish -it), opening the tablet's front camera ourselves (camera permission, the in-use dot, and the -camera's power), or sunrise and sunset from GNSS position and time (no sensor; tunnels ignored). +exposed either (`screen_brightness` is the manual level). Google Maps here +reads no light sensor either (checked while it navigated in its night colours): it goes by +time and place, and tunnels from map data. + +**Sun, done (2026-10-15):** without a light sensor, auto follows the sun at the tablet's own +rough location (Android's coarse location, polled; kept across restarts; not the Orin's GNSS, +since the tablet has its own GNSS and cellular): dark below −4° (`theme/sun.dart`). Android's +dark theme is the last fallback, before any location is known. Tunnels and car parks are not +seen; the quick button covers them. What was asked: Today the variant is a switch (the Dash's top-right button, and Settings). Wanted: diff --git a/lib/README.md b/lib/README.md index 1430a01..02c6117 100644 --- a/lib/README.md +++ b/lib/README.md @@ -42,7 +42,7 @@ pirate-station-aide's (main views, then the rarely used ones grouped as Tools). `onFill(colour)` (text on a fill). No widget names a colour. Themes are JSON in `assets/themes/`, `rei` by default; smart-serow's four-key files still load (their `highlight` is `alarm`). - **Dark or bright.** `Themes.mode` is `auto` (a light sensor where the device allows one, `theme/light.dart`; - else Android's dark theme, schedule included), `dark` or + else the sun at the tablet's own location, `theme/sun.dart`; else Android's dark theme), `dark` or `bright`, chosen in Settings and kept across restarts. The status bar's button holds the other variant; a long press there is auto again. - **Cosmetic fonts are opt-in.** A widget that wants the HUD face names `fontFamily: hudFont` in its own diff --git a/lib/main.dart b/lib/main.dart index 8fc5644..17ebb8e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,16 +1,18 @@ // Yukart display: the tablet app. Starts the link to the Orin and hands the screen to // the shell (lib/shell.dart). Code layout: lib/README.md. +import 'dart:async'; + import 'package:flutter/material.dart'; import 'link/boundary.dart'; import 'link/net.dart'; -import 'link/reading.dart'; import 'pages/link_test.dart'; import 'shell.dart'; import 'shell_scope.dart'; import 'theme/light.dart'; import 'theme/palette.dart'; +import 'theme/sun.dart'; /// Override for development, e.g. `--dart-define=YUKART_GW=ws://halogen.local:7700/`. const defaultUrl = String.fromEnvironment( @@ -25,14 +27,15 @@ Future main() async { final usb = UsbLink(); await usb.bindFor(Uri.parse(defaultUrl)); final boundary = Boundary(defaultUrl)..start(); - // The sun, for auto dark/bright: the car's position while GNSS has one. - boundary.addListener(() { - final lat = Reading.of(boundary, 'gnss_lat'); - final lon = Reading.of(boundary, 'gnss_lon'); - if (!lat.dim && !lon.dim && lat.number != null && lon.number != null) { - themes.setPosition(lat.number!, lon.number!); - } - }); + // The sun, for auto dark/bright: the tablet's own location, every minute until there + // is one (the first ask brings up the permission), then every ten. + Future locate() async { + final p = await androidLocation(); + if (p != null) themes.setPosition(p.$1, p.$2); + Timer(Duration(minutes: p == null ? 1 : 10), locate); + } + + locate(); final linkTest = LinkTest(usb); // Development hook: `am start ... --es linktest [dl:|bi:]