@@ -48,6 +48,13 @@ import androidx.glance.text.FontWeight
4848import androidx.glance.text.Text
4949import androidx.glance.text.TextStyle
5050import androidx.glance.unit.ColorProvider
51+ import androidx.work.Constraints
52+ import androidx.work.CoroutineWorker
53+ import androidx.work.ExistingWorkPolicy
54+ import androidx.work.NetworkType
55+ import androidx.work.OneTimeWorkRequestBuilder
56+ import androidx.work.WorkManager
57+ import androidx.work.WorkerParameters
5158import kotlinx.coroutines.CoroutineScope
5259import kotlinx.coroutines.Dispatchers
5360import kotlinx.coroutines.launch
@@ -57,8 +64,6 @@ import nz.aryan.akllive.R
5764import nz.aryan.akllive.data.AtApi
5865import nz.aryan.akllive.data.MapData
5966import nz.aryan.akllive.data.Nz
60- import nz.aryan.akllive.data.StopDeparture
61- import nz.aryan.akllive.data.StopRepo
6267import nz.aryan.akllive.data.Train
6368import nz.aryan.akllive.data.TrainRepo
6469import nz.aryan.akllive.ui.TrainArt
@@ -68,7 +73,7 @@ private fun open(ctx: Context, link: String) =
6873
6974// ======================= search =======================
7075
71- /* * A search bar on the home screen: straight into search, or plan a trip . */
76+ /* * A search bar on the home screen, like Google's: tap it and you're typing in the app's search . */
7277class SearchWidget : GlanceAppWidget () {
7378 override val sizeMode = SizeMode .Exact
7479
@@ -78,21 +83,18 @@ class SearchWidget : GlanceAppWidget() {
7883
7984 @Composable
8085 private fun Body (ctx : Context ) {
81- val wide = LocalSize .current.width > 250 .dp
82- Row (GlanceModifier .fillMaxSize().cornerRadius(28 .dp).background(GlanceTheme .colors.widgetBackground).padding(6 .dp),
83- verticalAlignment = Alignment .CenterVertically ) {
84- Row (GlanceModifier .defaultWeight().height(44 .dp).cornerRadius(22 .dp).background(GlanceTheme .colors.secondaryContainer)
85- .padding(horizontal = 14 .dp).clickable(open(ctx, " akllive://search" )),
86+ val size = LocalSize .current
87+ val wide = size.width > 220 .dp
88+ val h = minOf(size.height - 8 .dp, 56 .dp)
89+ Box (GlanceModifier .fillMaxSize(), contentAlignment = Alignment .Center ) {
90+ Row (GlanceModifier .fillMaxWidth().height(h).cornerRadius(28 .dp).background(GlanceTheme .colors.surface)
91+ .padding(start = 10 .dp, end = 16 .dp).clickable(open(ctx, " akllive://search" )),
8692 verticalAlignment = Alignment .CenterVertically ) {
87- Image (ImageProvider (R .drawable.ic_sc_search), null , GlanceModifier .size(22 .dp))
88- Spacer (GlanceModifier .width(10 .dp))
89- Text (if (wide) " Stops, places, routes, buses…" else " Search" , maxLines = 1 ,
90- style = TextStyle (color = GlanceTheme .colors.onSecondaryContainer, fontSize = 14 .sp))
91- }
92- Spacer (GlanceModifier .width(6 .dp))
93- Box (GlanceModifier .size(44 .dp).cornerRadius(22 .dp).background(GlanceTheme .colors.primary).clickable(open(ctx, " akllive://plan" )),
94- contentAlignment = Alignment .Center ) {
95- Image (ImageProvider (R .drawable.ic_sc_plan), " Plan a trip" , GlanceModifier .size(26 .dp))
93+ Image (ImageProvider (R .drawable.ic_widget_logo), " AKL Live" , GlanceModifier .size(30 .dp))
94+ Spacer (GlanceModifier .width(12 .dp))
95+ Text (if (wide) " Search stops, places, routes" else " Search" , GlanceModifier .defaultWeight(), maxLines = 1 ,
96+ style = TextStyle (color = GlanceTheme .colors.onSurfaceVariant, fontSize = 16 .sp))
97+ Image (ImageProvider (R .drawable.ic_widget_glass), null , GlanceModifier .size(22 .dp))
9698 }
9799 }
98100 }
@@ -102,67 +104,6 @@ class SearchWidgetReceiver : GlanceAppWidgetReceiver() {
102104 override val glanceAppWidget: GlanceAppWidget = SearchWidget ()
103105}
104106
105- // ======================= favourite stops =======================
106-
107- /* * Your favourite stops, each with its next departures. */
108- class FavouritesWidget : GlanceAppWidget () {
109- override val sizeMode = SizeMode .Exact
110-
111- override suspend fun provideGlance (context : Context , id : GlanceId ) {
112- val s = Prefs (context).load()
113- val favs = s.favs.take(3 )
114- val repo = StopRepo (AtApi { s.apiKey })
115- val deps = if (s.apiKey.isBlank()) emptyMap() else favs.associate { f ->
116- f.id to (try { repo.departures(mapOf (f.id to " " )) } catch (_: Exception ) { null })
117- }
118- provideContent { GlanceTheme { Body (context, favs.map { it.id to it.title }, deps, s.apiKey.isBlank()) } }
119- }
120-
121- @Composable
122- private fun Body (ctx : Context , favs : List <Pair <String , String >>, deps : Map <String , List <StopDeparture >? >, noKey : Boolean ) {
123- val now = Nz .nowSec()
124- val rows = if (LocalSize .current.height < 170 .dp) 1 else 2
125- Column (GlanceModifier .fillMaxSize().cornerRadius(24 .dp).background(GlanceTheme .colors.widgetBackground).padding(14 .dp)
126- .clickable(open(ctx, " akllive://home" ))) {
127- Row (GlanceModifier .fillMaxWidth(), verticalAlignment = Alignment .CenterVertically ) {
128- Text (" ★ Favourite stops" , GlanceModifier .defaultWeight(),
129- style = TextStyle (color = GlanceTheme .colors.onSurface, fontWeight = FontWeight .Bold , fontSize = 14 .sp))
130- Text (" ↻" , GlanceModifier .clickable(actionRunCallback<RefreshMore >()),
131- style = TextStyle (color = GlanceTheme .colors.primary, fontWeight = FontWeight .Bold , fontSize = 18 .sp))
132- }
133- when {
134- noKey -> Text (" Open AKL Live and add your AT key" , style = TextStyle (color = GlanceTheme .colors.onSurfaceVariant, fontSize = 13 .sp))
135- favs.isEmpty() -> Text (" Star a stop in the app and it shows up here" , style = TextStyle (color = GlanceTheme .colors.onSurfaceVariant, fontSize = 13 .sp))
136- else -> for ((id, title) in favs) {
137- Spacer (GlanceModifier .height(8 .dp))
138- Column (GlanceModifier .fillMaxWidth().clickable(open(ctx, " akllive://stop/" + Uri .encode(id)))) {
139- Text (title, maxLines = 1 , style = TextStyle (color = GlanceTheme .colors.onSurfaceVariant, fontSize = 11 .sp, fontWeight = FontWeight .Medium ))
140- val list = deps[id]
141- if (list.isNullOrEmpty()) Text (if (list == null ) " Couldn't load" else " Nothing for a while" ,
142- style = TextStyle (color = GlanceTheme .colors.onSurfaceVariant, fontSize = 12 .sp))
143- else list.filter { ! it.cancelled }.take(rows).forEach { d ->
144- Row (GlanceModifier .fillMaxWidth().padding(top = 2 .dp), verticalAlignment = Alignment .CenterVertically ) {
145- Box (GlanceModifier .cornerRadius(8 .dp).background(GlanceTheme .colors.primary).padding(horizontal = 6 .dp, vertical = 1 .dp)) {
146- Text (d.route, style = TextStyle (color = GlanceTheme .colors.onPrimary, fontWeight = FontWeight .Bold , fontSize = 12 .sp))
147- }
148- Spacer (GlanceModifier .width(6 .dp))
149- Text (d.headsign, GlanceModifier .defaultWeight(), maxLines = 1 ,
150- style = TextStyle (color = GlanceTheme .colors.onSurface, fontSize = 12 .sp))
151- Text (mins(d.expected - now), style = TextStyle (color = GlanceTheme .colors.primary, fontWeight = FontWeight .Bold , fontSize = 13 .sp))
152- }
153- }
154- }
155- }
156- }
157- }
158- }
159- }
160-
161- class FavouritesWidgetReceiver : GlanceAppWidgetReceiver () {
162- override val glanceAppWidget: GlanceAppWidget = FavouritesWidget ()
163- override fun onEnabled (context : Context ) { super .onEnabled(context); Glance .schedule(context) }
164- }
165-
166107// ======================= the train map =======================
167108
168109/* * The train map, as the app draws it, with every train on it, live. */
@@ -200,7 +141,7 @@ class TrainWidget : GlanceAppWidget() {
200141 Text (" Ngā Tereina" , GlanceModifier .defaultWeight(),
201142 style = TextStyle (color = ColorProvider (Color .White ), fontWeight = FontWeight .Bold , fontSize = 15 .sp))
202143 Text (when {
203- trains == null -> " Open the app first "
144+ trains == null -> " Tap ↻ for trains "
204145 else -> " ${trains.size} running · ${Nz .time(TrainsNow .at)} "
205146 }, style = TextStyle (color = ColorProvider (Color (0xFFE6ECF5 )), fontSize = 11 .sp))
206147 Spacer (GlanceModifier .width(8 .dp))
@@ -242,8 +183,26 @@ object TrainsNow {
242183 }
243184 }
244185
245- suspend fun get (ctx : Context ): List <Train >? {
246- if (list != null && Nz .nowSec() - at < 60 ) return list
186+ /* * The app's been opened: a good time to draw the widget's map, with the internet sure to be there. */
187+ fun appOpened (ctx : Context ) {
188+ val now = System .currentTimeMillis()
189+ if (now - lastOpen < 10 * 60_000 ) return
190+ lastOpen = now
191+ val app = ctx.applicationContext
192+ CoroutineScope (Dispatchers .Default ).launch { try { TrainWidget ().updateAll(app) } catch (_: Exception ) { } }
193+ }
194+ private var lastOpen = 0L
195+
196+ /* * Fetch and redraw in a job that waits for the internet. */
197+ fun refresh (ctx : Context ) {
198+ val req = OneTimeWorkRequestBuilder <TrainWidgetWorker >()
199+ .setConstraints(Constraints .Builder ().setRequiredNetworkType(NetworkType .CONNECTED ).build())
200+ .build()
201+ WorkManager .getInstance(ctx).enqueueUniqueWork(" train-widget-refresh" , ExistingWorkPolicy .REPLACE , req)
202+ }
203+
204+ suspend fun get (ctx : Context , fresh : Boolean = false): List <Train >? {
205+ if (! fresh && list != null && Nz .nowSec() - at < 60 ) return list
247206 val s = Prefs (ctx).load()
248207 if (s.apiKey.isBlank()) return list
249208 val r = repo ? : TrainRepo (AtApi { Prefs (ctx).load().apiKey }).also { repo = it }
@@ -300,16 +259,18 @@ private fun drawTrains(trains: List<Train>): Bitmap {
300259 return bmp
301260}
302261
303- private fun mins (secs : Long ) = when {
304- secs < 45 -> " Due"
305- secs < 3600 -> " ${secs / 60 } min"
306- else -> " ${secs / 3600 } h"
307- }
308-
309- /* * ↻ on the favourites or train widget. */
262+ /* * ↻ on the train widget: fetched in a job, which gets the internet even when the app's in the background. */
310263class RefreshMore : ActionCallback {
311264 override suspend fun onAction (context : Context , glanceId : GlanceId , parameters : ActionParameters ) {
312- FavouritesWidget ().updateAll(context)
313- TrainWidget ().updateAll(context)
265+ TrainsNow .refresh(context)
266+ }
267+ }
268+
269+ /* * Fetches the trains and redraws the train widget. */
270+ class TrainWidgetWorker (ctx : Context , params : WorkerParameters ) : CoroutineWorker(ctx, params) {
271+ override suspend fun doWork (): Result {
272+ TrainsNow .get(applicationContext, fresh = true )
273+ TrainWidget ().updateAll(applicationContext)
274+ return Result .success()
314275 }
315276}
0 commit comments