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:]