feat(widgets): stopwatch style picker and widget-picker previews
The stopwatch widget gets a per-instance style, chosen in a new StopwatchConfigActivity on the APPWIDGET_CONFIGURE contract. Every widget receiver now carries its own label, with a separate description, and the widget picker shows sample-value preview layouts instead of a bare name.
This commit is contained in:
@@ -258,8 +258,19 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- The stopwatch widget's per-instance style picker: the same
|
||||
APPWIDGET_CONFIGURE contract as the two above. -->
|
||||
<activity
|
||||
android:name=".ui.widget.config.StopwatchConfigActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Clockula">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- The quick-timer widget's per-instance duration picker (M11): the
|
||||
same APPWIDGET_CONFIGURE contract as the two above. -->
|
||||
same APPWIDGET_CONFIGURE contract as the three above. -->
|
||||
<activity
|
||||
android:name=".ui.widget.config.QuickTimerConfigActivity"
|
||||
android:exported="true"
|
||||
@@ -289,6 +300,7 @@
|
||||
change (widget/AnalogDateRefresh.kt). -->
|
||||
<receiver
|
||||
android:name=".widget.NextAlarmWidgetReceiver"
|
||||
android:label="@string/widget_next_alarm_label"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -300,6 +312,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widget.TimerWidgetReceiver"
|
||||
android:label="@string/widget_timer_label"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -311,6 +324,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widget.ClockWidgetReceiver"
|
||||
android:label="@string/widget_clock_label"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -322,6 +336,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widget.AnalogClockWidgetReceiver"
|
||||
android:label="@string/widget_analog_label"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -333,6 +348,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widget.StopwatchWidgetReceiver"
|
||||
android:label="@string/widget_stopwatch_label"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -344,6 +360,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widget.QuickTimerWidgetReceiver"
|
||||
android:label="@string/widget_quick_timer_label"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.jeanlucmakiola.clockula.domain.widget
|
||||
|
||||
/**
|
||||
* The faces a [de.jeanlucmakiola.clockula.widget.StopwatchWidget] instance can
|
||||
* render. [CARD] is the original full-widget card; every other style is an M3
|
||||
* Expressive shape floating on the home screen, with the start/pause button
|
||||
* tucked against its lower-left edge and lap/reset in a pill at its upper
|
||||
* right. See `ui/widget/StopwatchWidgetContent.kt` for the mapping.
|
||||
*/
|
||||
enum class StopwatchWidgetStyle { CARD, CIRCLE, COOKIE, SUNNY, SQUARE }
|
||||
|
||||
/** Anything unreadable, or a name this enum no longer has, falls back to [StopwatchWidgetStyle.CARD] — what every instance placed before styles existed shows. */
|
||||
fun parseStopwatchWidgetStyle(raw: String?): StopwatchWidgetStyle =
|
||||
raw?.let { runCatching { StopwatchWidgetStyle.valueOf(it) }.getOrNull() } ?: StopwatchWidgetStyle.CARD
|
||||
@@ -2,21 +2,31 @@ package de.jeanlucmakiola.clockula.ui.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.glance.ColorFilter
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.Image
|
||||
import androidx.glance.ImageProvider
|
||||
import androidx.glance.LocalContext
|
||||
import androidx.glance.LocalSize
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.action.actionSendBroadcast
|
||||
import androidx.glance.appwidget.action.actionStartActivity
|
||||
import androidx.glance.background
|
||||
import androidx.glance.currentState
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.Spacer
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.height
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.size
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
@@ -26,6 +36,9 @@ import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.domain.StopwatchState
|
||||
import de.jeanlucmakiola.clockula.domain.format.ClockFormat
|
||||
import de.jeanlucmakiola.clockula.domain.widget.StopwatchWidgetModel
|
||||
import de.jeanlucmakiola.clockula.domain.widget.StopwatchWidgetStyle
|
||||
import de.jeanlucmakiola.clockula.domain.widget.parseStopwatchWidgetStyle
|
||||
import de.jeanlucmakiola.clockula.widget.WIDGET_STOPWATCH_STYLE_KEY
|
||||
import de.jeanlucmakiola.clockula.stopwatch.StopwatchIntents
|
||||
import de.jeanlucmakiola.clockula.stopwatch.receiver.StopwatchActionReceiver
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -37,14 +50,16 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.collectAsState
|
||||
|
||||
/**
|
||||
* Reads the stopwatch run and its laps reactively, then renders [StopwatchWidgetContent].
|
||||
* Collected *inside* the composition: `update()` on a still-live session (one
|
||||
* lives ~45s) only recomposes, so a value read once before `provideContent`
|
||||
* would keep showing what was true when the session started — the
|
||||
* `initial` value only makes the first frame right.
|
||||
* Reads this instance's style, the stopwatch run and its laps reactively,
|
||||
* then renders [StopwatchWidgetContent]. Collected *inside* the composition:
|
||||
* `update()` on a still-live session (one lives ~45s) only recomposes, so a
|
||||
* value read once before `provideContent` would keep showing what was true
|
||||
* when the session started — the `initial` value only makes the first frame
|
||||
* right.
|
||||
*/
|
||||
@Composable
|
||||
fun StopwatchWidgetRoot(entryPoint: WidgetEntryPoint, initialRun: StopwatchRun, initialLapCount: Int) {
|
||||
val style = parseStopwatchWidgetStyle(currentState<Preferences>()[WIDGET_STOPWATCH_STYLE_KEY])
|
||||
val repository = entryPoint.stopwatchRepository()
|
||||
val run by remember { repository.run() }.collectAsState(initial = initialRun)
|
||||
val lapCount by remember { repository.laps().map { it.size } }.collectAsState(initial = initialLapCount)
|
||||
@@ -52,19 +67,34 @@ fun StopwatchWidgetRoot(entryPoint: WidgetEntryPoint, initialRun: StopwatchRun,
|
||||
// is exactly `now - elapsed`.
|
||||
val elapsedRealtime = remember(run, lapCount) { entryPoint.elapsedRealtimeClock().elapsedRealtime() }
|
||||
val model = remember(run, lapCount) { StopwatchWidgetPresenter.present(run, elapsedRealtime, lapCount) }
|
||||
StopwatchWidgetContent(model, elapsedRealtime.inWholeMilliseconds)
|
||||
StopwatchWidgetContent(model, elapsedRealtime.inWholeMilliseconds, style)
|
||||
}
|
||||
|
||||
/**
|
||||
* The stopwatch widget's content, given an already-resolved [model]: the
|
||||
* readout as large as the widget allows, a wide pill for start/pause and a
|
||||
* round lap/reset beside it. Below [WIDE_BELOW_HEIGHT] the controls move
|
||||
* beside the readout instead of under it. No 1s ticker — a running readout
|
||||
* is the platform chronometer, based at [elapsedRealtimeNow] (the
|
||||
* `SystemClock.elapsedRealtime()` millis the model was resolved at).
|
||||
* The stopwatch widget's content, given an already-resolved [model], in
|
||||
* [style]: [CardFace] or, for every other style, [ShapeFace]. No 1s ticker —
|
||||
* a running readout is the platform chronometer, based at
|
||||
* [elapsedRealtimeNow] (the `SystemClock.elapsedRealtime()` millis the model
|
||||
* was resolved at).
|
||||
*/
|
||||
@Composable
|
||||
fun StopwatchWidgetContent(model: StopwatchWidgetModel, elapsedRealtimeNow: Long) {
|
||||
fun StopwatchWidgetContent(model: StopwatchWidgetModel, elapsedRealtimeNow: Long, style: StopwatchWidgetStyle) {
|
||||
when (style) {
|
||||
StopwatchWidgetStyle.CARD -> CardFace(model, elapsedRealtimeNow)
|
||||
StopwatchWidgetStyle.CIRCLE -> ShapeFace(model, elapsedRealtimeNow, R.drawable.widget_bg_circle, ROUND_INSET)
|
||||
StopwatchWidgetStyle.COOKIE -> ShapeFace(model, elapsedRealtimeNow, R.drawable.widget_shape_cookie, ROUND_INSET)
|
||||
StopwatchWidgetStyle.SUNNY -> ShapeFace(model, elapsedRealtimeNow, R.drawable.widget_shape_sunny, ROUND_INSET)
|
||||
StopwatchWidgetStyle.SQUARE -> ShapeFace(model, elapsedRealtimeNow, R.drawable.widget_shape_square, SQUARE_INSET)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The card: the readout as large as the widget allows, a wide pill for
|
||||
* start/pause and a round lap/reset beside it. Below [WIDE_BELOW_HEIGHT] the
|
||||
* controls move beside the readout instead of under it.
|
||||
*/
|
||||
@Composable
|
||||
private fun CardFace(model: StopwatchWidgetModel, elapsedRealtimeNow: Long) {
|
||||
val context = LocalContext.current
|
||||
val size = LocalSize.current
|
||||
val w = size.width.value
|
||||
@@ -118,12 +148,121 @@ fun StopwatchWidgetContent(model: StopwatchWidgetModel, elapsedRealtimeNow: Long
|
||||
/** Under this height the controls sit beside the readout. */
|
||||
private const val WIDE_BELOW_HEIGHT = 120f
|
||||
|
||||
/**
|
||||
* A [shape] floating on the home screen, like the analog faces, holding the
|
||||
* readout. Start/pause is a button of its own overlapping the shape's
|
||||
* lower-left edge — a rounded square to start or resume that rounds into a
|
||||
* circle while it runs — and, once the stopwatch has run, lap (while running)
|
||||
* and reset share a tertiary pill at its upper right. Both are pulled in from
|
||||
* the square's corners by [inset] (a fraction of its side), so they straddle
|
||||
* the shape's edge rather than float clear of it. Everything is a fraction of
|
||||
* the square the widget fits, so the face scales as one piece.
|
||||
*/
|
||||
@Composable
|
||||
private fun Readout(context: Context, model: StopwatchWidgetModel, elapsedRealtimeNow: Long, sizeDp: Float) {
|
||||
private fun ShapeFace(model: StopwatchWidgetModel, elapsedRealtimeNow: Long, @DrawableRes shape: Int, inset: Float) {
|
||||
val context = LocalContext.current
|
||||
val size = LocalSize.current
|
||||
val side = minOf(size.width, size.height)
|
||||
val s = side.value
|
||||
val running = model.mode == StopwatchState.RUNNING
|
||||
// The shape leaves the square's corners free, and the buttons sit there.
|
||||
val body = s * 0.84f
|
||||
val readoutChars = maxOf(5, ClockFormat.elapsed(model.elapsed).length)
|
||||
// Fits the shape's inner ~62% — Sunny's and Cookie's lobes cut in deeper than a circle's edge.
|
||||
val timeSize = minOf(s * 0.17f, body * 0.62f / (readoutChars * 0.6f))
|
||||
val button = s * 0.28f
|
||||
val action = s * 0.2f
|
||||
|
||||
CenteredSquare(side = side, onClick = actionStartActivity(showStopwatchIntent(context))) {
|
||||
Box(modifier = GlanceModifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(
|
||||
modifier = GlanceModifier
|
||||
.size(body.dp)
|
||||
.background(ImageProvider(shape), colorFilter = ColorFilter.tint(GlanceTheme.colors.primaryContainer)),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Readout(context, model, elapsedRealtimeNow, timeSize, onShape = true)
|
||||
if (model.lapCount > 0) {
|
||||
Text(
|
||||
text = context.getString(R.string.widget_stopwatch_laps, model.lapCount),
|
||||
style = TextStyle(
|
||||
color = GlanceTheme.colors.onPrimaryContainer,
|
||||
fontSize = fittedSp(context, (s * 0.065f).coerceAtLeast(10f)),
|
||||
fontWeight = FontWeight.Medium,
|
||||
),
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(modifier = GlanceModifier.fillMaxSize().padding((s * inset).dp), contentAlignment = Alignment.BottomStart) {
|
||||
ShapedIcon(
|
||||
icon = if (running) R.drawable.widget_ic_pause else R.drawable.widget_ic_play,
|
||||
contentDescription = context.getString(startPauseLabel(model.mode)),
|
||||
width = button.dp,
|
||||
height = button.dp,
|
||||
container = GlanceTheme.colors.primary,
|
||||
content = GlanceTheme.colors.onPrimary,
|
||||
shape = if (running) R.drawable.widget_bg_circle else R.drawable.widget_shape_square,
|
||||
iconFraction = 0.4f,
|
||||
onClick = actionSendBroadcast(
|
||||
actionIntent(context, if (running) StopwatchIntents.ACTION_PAUSE else StopwatchIntents.ACTION_RESUME),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (model.mode != StopwatchState.IDLE) {
|
||||
Box(modifier = GlanceModifier.fillMaxSize().padding((s * inset).dp), contentAlignment = Alignment.TopEnd) {
|
||||
Row(
|
||||
modifier = GlanceModifier
|
||||
.padding(horizontal = (action * 0.15f).dp)
|
||||
.background(ImageProvider(R.drawable.widget_bg_pill), colorFilter = ColorFilter.tint(GlanceTheme.colors.tertiaryContainer)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (running) {
|
||||
PillAction(R.drawable.widget_ic_flag, context.getString(R.string.stopwatch_lap), action, StopwatchIntents.ACTION_LAP)
|
||||
}
|
||||
PillAction(R.drawable.widget_ic_replay, context.getString(R.string.stopwatch_reset), action, StopwatchIntents.ACTION_RESET)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [ShapeFace]'s control insets. A round shape's edge on the diagonal is ~0.2
|
||||
* of the side in from the square's corner, so its controls come in further
|
||||
* than the Square's, whose rounded corner nearly fills the square's own.
|
||||
*/
|
||||
private const val ROUND_INSET = 0.065f
|
||||
private const val SQUARE_INSET = 0.025f
|
||||
|
||||
/** One of [ShapeFace]'s pill actions: a bare glyph on the pill's own colour, its whole [size] the touch target. */
|
||||
@Composable
|
||||
private fun PillAction(@DrawableRes icon: Int, description: String, size: Float, action: String) {
|
||||
val context = LocalContext.current
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.size(size.dp)
|
||||
.clickable(actionSendBroadcast(actionIntent(context, action))),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
provider = ImageProvider(icon),
|
||||
contentDescription = description,
|
||||
modifier = GlanceModifier.size((size * 0.5f).dp),
|
||||
colorFilter = ColorFilter.tint(GlanceTheme.colors.onTertiaryContainer),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** [onShape]: the readout sits on [ShapeFace]'s primary-container shape rather than the card's surface. */
|
||||
@Composable
|
||||
private fun Readout(context: Context, model: StopwatchWidgetModel, elapsedRealtimeNow: Long, sizeDp: Float, onShape: Boolean = false) {
|
||||
if (model.mode == StopwatchState.RUNNING) {
|
||||
WidgetChronometer(
|
||||
layoutRes = R.layout.widget_chronometer,
|
||||
viewId = R.id.widget_chronometer,
|
||||
layoutRes = if (onShape) R.layout.widget_chronometer_on_primary else R.layout.widget_chronometer,
|
||||
viewId = if (onShape) R.id.widget_chronometer_on_primary else R.id.widget_chronometer,
|
||||
base = elapsedRealtimeNow - model.elapsed.inWholeMilliseconds,
|
||||
countDown = false,
|
||||
textSizeDp = sizeDp,
|
||||
@@ -132,11 +271,21 @@ private fun Readout(context: Context, model: StopwatchWidgetModel, elapsedRealti
|
||||
WidgetReadout(
|
||||
text = ClockFormat.elapsed(model.elapsed),
|
||||
textSizeDp = sizeDp,
|
||||
colorRes = if (model.mode == StopwatchState.IDLE) R.color.widget_on_surface_variant else R.color.widget_on_surface,
|
||||
colorRes = when {
|
||||
onShape -> R.color.widget_on_primary_container
|
||||
model.mode == StopwatchState.IDLE -> R.color.widget_on_surface_variant
|
||||
else -> R.color.widget_on_surface
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPauseLabel(mode: StopwatchState): Int = when (mode) {
|
||||
StopwatchState.IDLE -> R.string.stopwatch_start
|
||||
StopwatchState.RUNNING -> R.string.stopwatch_pause
|
||||
StopwatchState.PAUSED -> R.string.stopwatch_resume
|
||||
}
|
||||
|
||||
/** A wide primary pill (start/pause) and, once the stopwatch has run, a round lap/reset beside it. */
|
||||
@Composable
|
||||
private fun Controls(context: Context, model: StopwatchWidgetModel, button: Float) {
|
||||
@@ -144,13 +293,7 @@ private fun Controls(context: Context, model: StopwatchWidgetModel, button: Floa
|
||||
val running = model.mode == StopwatchState.RUNNING
|
||||
ShapedIcon(
|
||||
icon = if (running) R.drawable.widget_ic_pause else R.drawable.widget_ic_play,
|
||||
contentDescription = context.getString(
|
||||
when (model.mode) {
|
||||
StopwatchState.IDLE -> R.string.stopwatch_start
|
||||
StopwatchState.RUNNING -> R.string.stopwatch_pause
|
||||
StopwatchState.PAUSED -> R.string.stopwatch_resume
|
||||
},
|
||||
),
|
||||
contentDescription = context.getString(startPauseLabel(model.mode)),
|
||||
width = (button * 1.6f).dp,
|
||||
height = button.dp,
|
||||
container = GlanceTheme.colors.primary,
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package de.jeanlucmakiola.clockula.ui.widget.config
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.view.MotionEvent
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.RemoteViews
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||
import androidx.glance.appwidget.GlanceRemoteViews
|
||||
import androidx.glance.appwidget.state.getAppWidgetState
|
||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.domain.StopwatchState
|
||||
import de.jeanlucmakiola.clockula.domain.widget.StopwatchWidgetModel
|
||||
import de.jeanlucmakiola.clockula.domain.widget.StopwatchWidgetStyle
|
||||
import de.jeanlucmakiola.clockula.domain.widget.parseStopwatchWidgetStyle
|
||||
import de.jeanlucmakiola.clockula.ui.widget.StopwatchWidgetContent
|
||||
import de.jeanlucmakiola.clockula.widget.StopwatchWidget
|
||||
import de.jeanlucmakiola.clockula.widget.WIDGET_STOPWATCH_STYLE_KEY
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* The stopwatch widget's entire configuration: a style pick. Like
|
||||
* [AnalogClockConfigActivity], tapping an option saves and finishes
|
||||
* immediately.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class StopwatchConfigActivity : WidgetConfigActivity() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
var style by remember { mutableStateOf(StopwatchWidgetStyle.CARD) }
|
||||
LaunchedEffect(glanceId) {
|
||||
val state = getAppWidgetState(this@StopwatchConfigActivity, PreferencesGlanceStateDefinition, glanceId)
|
||||
style = parseStopwatchWidgetStyle(state[WIDGET_STOPWATCH_STYLE_KEY])
|
||||
}
|
||||
WidgetStylePicker(
|
||||
title = stringResource(R.string.widget_stopwatch_config_title),
|
||||
options = StopwatchWidgetStyle.entries,
|
||||
selected = style,
|
||||
label = { stringResource(stopwatchStyleLabel(it)) },
|
||||
preview = { StopwatchStylePreview(it) },
|
||||
onSelect = { picked ->
|
||||
style = picked
|
||||
save {
|
||||
updateAppWidgetState(this@StopwatchConfigActivity, glanceId) { prefs ->
|
||||
prefs[WIDGET_STOPWATCH_STYLE_KEY] = picked.name
|
||||
}
|
||||
StopwatchWidget().update(this@StopwatchConfigActivity, glanceId)
|
||||
}
|
||||
},
|
||||
onDismiss = ::dismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun stopwatchStyleLabel(style: StopwatchWidgetStyle): Int = when (style) {
|
||||
StopwatchWidgetStyle.CARD -> R.string.stopwatch_style_card
|
||||
StopwatchWidgetStyle.CIRCLE -> R.string.stopwatch_style_circle
|
||||
StopwatchWidgetStyle.COOKIE -> R.string.analog_style_cookie
|
||||
StopwatchWidgetStyle.SUNNY -> R.string.analog_style_sunny
|
||||
StopwatchWidgetStyle.SQUARE -> R.string.analog_style_square
|
||||
}
|
||||
|
||||
/**
|
||||
* The real widget face, not a redrawing: [StopwatchWidgetContent] composed to
|
||||
* `RemoteViews` by Glance itself and inflated in-app, showing a running
|
||||
* stopwatch with laps banked — every control the face has. The chronometer
|
||||
* ticks as it would on the home screen.
|
||||
*/
|
||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||
@Composable
|
||||
private fun StopwatchStylePreview(style: StopwatchWidgetStyle) {
|
||||
val context = LocalContext.current
|
||||
val size = if (style == StopwatchWidgetStyle.CARD) CardPreviewSize else ShapePreviewSize
|
||||
val views by produceState<RemoteViews?>(initialValue = null, style) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
value = GlanceRemoteViews().compose(context, size) {
|
||||
StopwatchWidgetContent(PreviewModel, now, style)
|
||||
}.remoteViews
|
||||
}
|
||||
AndroidView(
|
||||
// Unlike the analog face, this one carries the widget's own click
|
||||
// actions (open the app, start, lap, reset); it must not take touches,
|
||||
// or a tap fires them instead of picking the style.
|
||||
factory = { ctx ->
|
||||
object : FrameLayout(ctx) {
|
||||
override fun dispatchTouchEvent(event: MotionEvent): Boolean = false
|
||||
}
|
||||
},
|
||||
update = { frame ->
|
||||
frame.removeAllViews()
|
||||
views?.let { frame.addView(it.apply(frame.context, frame)) }
|
||||
},
|
||||
modifier = Modifier.size(size),
|
||||
)
|
||||
}
|
||||
|
||||
private val PreviewModel = StopwatchWidgetModel(mode = StopwatchState.RUNNING, elapsed = 83.seconds, lapCount = 2)
|
||||
private val CardPreviewSize = DpSize(240.dp, 150.dp)
|
||||
private val ShapePreviewSize = DpSize(180.dp, 180.dp)
|
||||
@@ -20,6 +20,9 @@ internal val WIDGET_SHOW_WORLD_CLOCKS_KEY = booleanPreferencesKey("show_world_cl
|
||||
/** Per-instance Glance state key holding the analog widget's [AnalogClockStyle] ([name]). */
|
||||
internal val WIDGET_ANALOG_STYLE_KEY = stringPreferencesKey("analog_style")
|
||||
|
||||
/** Per-instance Glance state key holding the stopwatch widget's `StopwatchWidgetStyle` ([name]). */
|
||||
internal val WIDGET_STOPWATCH_STYLE_KEY = stringPreferencesKey("stopwatch_style")
|
||||
|
||||
/** Per-instance Glance state key holding the quick-timer widget's configured duration, in millis. */
|
||||
internal val WIDGET_QUICK_TIMER_DURATION_KEY = longPreferencesKey("quick_timer_duration_millis")
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- `MaterialShapes.Sunny` (`values/widget_shapes.xml`), white so its user
|
||||
tints it — see `widget_bg_card.xml`. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="100"
|
||||
android:viewportHeight="100">
|
||||
<path
|
||||
android:pathData="@string/widget_shape_path_sunny"
|
||||
android:fillColor="#FFFFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The widget picker's preview of the next-alarm widget (`previewLayout`,
|
||||
API 31+): a static stand-in for `ui/widget/NextAlarmWidgetContent.kt` —
|
||||
the Cookie alarm badge, a day, a time and a label on the surface card. -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/widget_bg_card"
|
||||
android:backgroundTint="@color/widget_surface"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="14dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:background="@drawable/widget_shape_cookie"
|
||||
android:backgroundTint="@color/widget_primary_container"
|
||||
android:importantForAccessibility="no"
|
||||
android:padding="13dp"
|
||||
android:src="@drawable/widget_ic_alarm"
|
||||
android:tint="@color/widget_on_primary_container" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:maxLines="1"
|
||||
android:text="@string/widget_next_alarm_today"
|
||||
android:textColor="@color/widget_primary"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:text="@string/widget_preview_alarm_time"
|
||||
android:textColor="@color/widget_on_surface"
|
||||
android:textSize="30sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The widget picker's preview of the quick-timer widget (`previewLayout`,
|
||||
API 31+): a static stand-in for `ui/widget/QuickTimerWidgetContent.kt`
|
||||
at rest — the Scallop with a play glyph over its default 05:00. -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/widget_shape_scallop"
|
||||
android:tint="@color/widget_primary_container" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/widget_ic_play"
|
||||
android:tint="@color/widget_on_primary_container" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:text="@string/widget_preview_quick_timer_time"
|
||||
android:textColor="@color/widget_on_primary_container"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The widget picker's preview of the stopwatch widget (`previewLayout`,
|
||||
API 31+): a static stand-in for `ui/widget/StopwatchWidgetContent.kt`'s
|
||||
Circle face, running — the shape with its readout, the circular pause
|
||||
against its lower-left edge, lap and reset in the pill at its upper
|
||||
right. Fixed at a size that fits two cells. -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="112dp"
|
||||
android:layout_height="112dp"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<TextView
|
||||
android:layout_width="94dp"
|
||||
android:layout_height="94dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/widget_bg_circle"
|
||||
android:backgroundTint="@color/widget_primary_container"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:text="@string/widget_preview_stopwatch_time"
|
||||
android:textColor="@color/widget_on_primary_container"
|
||||
android:textSize="19sp" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="31dp"
|
||||
android:layout_height="31dp"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:layout_margin="7dp"
|
||||
android:background="@drawable/widget_bg_circle"
|
||||
android:backgroundTint="@color/widget_primary"
|
||||
android:importantForAccessibility="no"
|
||||
android:padding="9dp"
|
||||
android:src="@drawable/widget_ic_pause"
|
||||
android:tint="@color/widget_on_primary" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="22dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_margin="7dp"
|
||||
android:background="@drawable/widget_bg_pill"
|
||||
android:backgroundTint="@color/widget_tertiary_container"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="4dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="22dp"
|
||||
android:layout_height="22dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:padding="5dp"
|
||||
android:src="@drawable/widget_ic_flag"
|
||||
android:tint="@color/widget_on_tertiary_container" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="22dp"
|
||||
android:layout_height="22dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:padding="5dp"
|
||||
android:src="@drawable/widget_ic_replay"
|
||||
android:tint="@color/widget_on_tertiary_container" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The widget picker's preview of the timer widget (`previewLayout`,
|
||||
API 31+): a static stand-in for `ui/widget/TimerWidgetContent.kt`'s lone
|
||||
running timer — readout over a circular pause and a reset. -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/widget_bg_card"
|
||||
android:backgroundTint="@color/widget_surface"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="14dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:text="@string/widget_preview_timer_time"
|
||||
android:textColor="@color/widget_on_surface"
|
||||
android:textSize="40sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:background="@drawable/widget_bg_circle"
|
||||
android:backgroundTint="@color/widget_primary"
|
||||
android:importantForAccessibility="no"
|
||||
android:padding="11dp"
|
||||
android:src="@drawable/widget_ic_pause"
|
||||
android:tint="@color/widget_on_primary" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/widget_bg_circle"
|
||||
android:backgroundTint="@color/widget_secondary_container"
|
||||
android:importantForAccessibility="no"
|
||||
android:padding="11dp"
|
||||
android:src="@drawable/widget_ic_replay"
|
||||
android:tint="@color/widget_on_secondary_container" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -18,6 +18,9 @@
|
||||
<color name="widget_primary">@android:color/system_accent1_200</color>
|
||||
<color name="widget_primary_container">@android:color/system_accent1_700</color>
|
||||
<color name="widget_on_primary_container">@android:color/system_accent1_100</color>
|
||||
<color name="widget_on_primary">@android:color/system_accent1_800</color>
|
||||
<color name="widget_secondary_container">@android:color/system_accent2_700</color>
|
||||
<color name="widget_on_secondary_container">@android:color/system_accent2_100</color>
|
||||
<color name="widget_tertiary">@android:color/system_accent3_200</color>
|
||||
<color name="widget_tertiary_container">@android:color/system_accent3_700</color>
|
||||
<color name="widget_on_tertiary_container">@android:color/system_accent3_100</color>
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
<color name="widget_primary">#FFCEE2BA</color>
|
||||
<color name="widget_primary_container">#FF4A583B</color>
|
||||
<color name="widget_on_primary_container">#FFE8F6D9</color>
|
||||
<color name="widget_on_primary">#FF213510</color>
|
||||
<color name="widget_secondary_container">#FF3D4A35</color>
|
||||
<color name="widget_on_secondary_container">#FFD8E7CB</color>
|
||||
<color name="widget_tertiary">#FFA5CEC7</color>
|
||||
<color name="widget_tertiary_container">#FF244D48</color>
|
||||
<color name="widget_on_tertiary_container">#FFC1ECE4</color>
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
<color name="widget_primary">@android:color/system_accent1_600</color>
|
||||
<color name="widget_primary_container">@android:color/system_accent1_100</color>
|
||||
<color name="widget_on_primary_container">@android:color/system_accent1_900</color>
|
||||
<color name="widget_on_primary">@android:color/system_accent1_0</color>
|
||||
<color name="widget_secondary_container">@android:color/system_accent2_100</color>
|
||||
<color name="widget_on_secondary_container">@android:color/system_accent2_900</color>
|
||||
<color name="widget_tertiary">@android:color/system_accent3_600</color>
|
||||
<color name="widget_tertiary_container">@android:color/system_accent3_100</color>
|
||||
<color name="widget_on_tertiary_container">@android:color/system_accent3_900</color>
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
<color name="widget_primary">#FF5C6A4E</color>
|
||||
<color name="widget_primary_container">#FFD7E8C6</color>
|
||||
<color name="widget_on_primary_container">#FF1D2A10</color>
|
||||
<color name="widget_on_primary">#FFFFFFFF</color>
|
||||
<color name="widget_secondary_container">#FFDCE7D0</color>
|
||||
<color name="widget_on_secondary_container">#FF161E10</color>
|
||||
<color name="widget_tertiary">#FF3D6660</color>
|
||||
<color name="widget_tertiary_container">#FFCCE8E3</color>
|
||||
<color name="widget_on_tertiary_container">#FF05201C</color>
|
||||
|
||||
@@ -358,13 +358,20 @@
|
||||
<string name="selfcheck_next_alarm_problem">Doesn\'t match what Clockula scheduled — open the app to re-check</string>
|
||||
|
||||
<!-- M10: widgets -->
|
||||
<string name="widget_next_alarm_description">Next alarm</string>
|
||||
<!-- Sample values for the widget picker's previews (`layout/widget_preview_*.xml`). -->
|
||||
<string name="widget_preview_alarm_time" translatable="false">7:00</string>
|
||||
<string name="widget_preview_timer_time" translatable="false">04:59</string>
|
||||
<string name="widget_preview_stopwatch_time" translatable="false">01:23</string>
|
||||
<string name="widget_preview_quick_timer_time" translatable="false">05:00</string>
|
||||
<string name="widget_next_alarm_label">Next alarm</string>
|
||||
<string name="widget_next_alarm_empty">No alarms set</string>
|
||||
<string name="widget_next_alarm_today">Today</string>
|
||||
<string name="widget_timer_description">Timer</string>
|
||||
<string name="widget_timer_label">Timer</string>
|
||||
<string name="widget_timer_description">Your running timers</string>
|
||||
<string name="widget_timer_new">New timer</string>
|
||||
<string name="widget_timer_expired">Done</string>
|
||||
<string name="widget_clock_description">Clock</string>
|
||||
<string name="widget_clock_label">Digital clock</string>
|
||||
<string name="widget_clock_description">With date and world clocks</string>
|
||||
|
||||
<!-- M11: digital clock widget faces + per-instance config -->
|
||||
<string name="widget_config_title">Digital clock</string>
|
||||
@@ -379,7 +386,7 @@
|
||||
<string name="widget_config_show_world_clocks_hint">Up to three cities from your World clock tab</string>
|
||||
|
||||
<!-- M11: analog clock widget -->
|
||||
<string name="widget_analog_description">Analog clock</string>
|
||||
<string name="widget_analog_label">Analog clock</string>
|
||||
<string name="widget_analog_config_title">Analog clock</string>
|
||||
<string name="analog_style_round">Round</string>
|
||||
<string name="analog_style_numeral">Numeral</string>
|
||||
@@ -392,11 +399,15 @@
|
||||
<string name="analog_style_scallop">Scallop</string>
|
||||
|
||||
<!-- M11: stopwatch widget -->
|
||||
<string name="widget_stopwatch_description">Stopwatch</string>
|
||||
<string name="widget_stopwatch_label">Stopwatch</string>
|
||||
<string name="widget_stopwatch_laps">Laps: %1$d</string>
|
||||
<string name="widget_stopwatch_config_title">Stopwatch</string>
|
||||
<string name="stopwatch_style_card">Card</string>
|
||||
<string name="stopwatch_style_circle">Circle</string>
|
||||
|
||||
<!-- M11: quick-timer widget -->
|
||||
<string name="widget_quick_timer_description">Quick timer</string>
|
||||
<string name="widget_quick_timer_label">Quick timer</string>
|
||||
<string name="widget_quick_timer_description">Starts a timer with one tap</string>
|
||||
<string name="widget_quick_timer_config_title">Quick timer</string>
|
||||
<string name="widget_quick_timer_start">Start %1$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
android:updatePeriodMillis="0"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:description="@string/widget_analog_description"
|
||||
android:configure="de.jeanlucmakiola.clockula.ui.widget.config.AnalogClockConfigActivity"
|
||||
android:widgetFeatures="reconfigurable|configuration_optional"
|
||||
android:widgetFeatures="reconfigurable"
|
||||
android:previewLayout="@layout/widget_analog_plain" />
|
||||
|
||||
@@ -10,4 +10,4 @@
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:previewImage="@mipmap/ic_launcher"
|
||||
android:description="@string/widget_next_alarm_description" />
|
||||
android:previewLayout="@layout/widget_preview_next_alarm" />
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:previewImage="@mipmap/ic_launcher"
|
||||
android:previewLayout="@layout/widget_preview_quick_timer"
|
||||
android:description="@string/widget_quick_timer_description"
|
||||
android:configure="de.jeanlucmakiola.clockula.ui.widget.config.QuickTimerConfigActivity"
|
||||
android:widgetFeatures="reconfigurable|configuration_optional" />
|
||||
|
||||
@@ -10,4 +10,6 @@
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:previewImage="@mipmap/ic_launcher"
|
||||
android:description="@string/widget_stopwatch_description" />
|
||||
android:previewLayout="@layout/widget_preview_stopwatch"
|
||||
android:configure="de.jeanlucmakiola.clockula.ui.widget.config.StopwatchConfigActivity"
|
||||
android:widgetFeatures="reconfigurable" />
|
||||
|
||||
@@ -10,4 +10,5 @@
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:previewImage="@mipmap/ic_launcher"
|
||||
android:previewLayout="@layout/widget_preview_timer"
|
||||
android:description="@string/widget_timer_description" />
|
||||
|
||||
@@ -31,7 +31,7 @@ class ManifestRulesTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exactly the launcher, the door, the three widget configurators and the six widget receivers are exported`() {
|
||||
fun `exactly the launcher, the door, the four widget configurators and the six widget receivers are exported`() {
|
||||
val exported = Regex("<(activity|service|receiver|provider)\\b[\\s\\S]*?(?:/>|</\\1>)")
|
||||
.findAll(text)
|
||||
.map { it.value }
|
||||
@@ -43,15 +43,16 @@ class ManifestRulesTest {
|
||||
// it as a widget provider at all, and `the six widget receivers
|
||||
// answer only APPWIDGET_UPDATE` pins that each one is guarded to
|
||||
// that one platform-delivered broadcast and nothing wider. M11 adds
|
||||
// three widget config activities: exported for the same reason —
|
||||
// four widget config activities: exported for the same reason —
|
||||
// only the launcher can start an `APPWIDGET_CONFIGURE` activity
|
||||
// cross-package — and guarded to that one action by `the three
|
||||
// cross-package — and guarded to that one action by `the four
|
||||
// widget configurators answer only APPWIDGET_CONFIGURE` below.
|
||||
assertThat(exported).containsExactly(
|
||||
".MainActivity",
|
||||
".interop.AlarmClockActivity",
|
||||
".ui.widget.config.ClockWidgetConfigActivity",
|
||||
".ui.widget.config.AnalogClockConfigActivity",
|
||||
".ui.widget.config.StopwatchConfigActivity",
|
||||
".ui.widget.config.QuickTimerConfigActivity",
|
||||
".widget.NextAlarmWidgetReceiver",
|
||||
".widget.TimerWidgetReceiver",
|
||||
@@ -64,14 +65,15 @@ class ManifestRulesTest {
|
||||
|
||||
/**
|
||||
* M11: the mirror of `the six widget receivers answer only
|
||||
* APPWIDGET_UPDATE` for the three widget config activities — being
|
||||
* APPWIDGET_UPDATE` for the four widget config activities — being
|
||||
* exported cannot be widened into a second, wider surface by accident.
|
||||
*/
|
||||
@Test
|
||||
fun `the three widget configurators answer only APPWIDGET_CONFIGURE`() {
|
||||
fun `the four widget configurators answer only APPWIDGET_CONFIGURE`() {
|
||||
val configurators = listOf(
|
||||
".ui.widget.config.ClockWidgetConfigActivity",
|
||||
".ui.widget.config.AnalogClockConfigActivity",
|
||||
".ui.widget.config.StopwatchConfigActivity",
|
||||
".ui.widget.config.QuickTimerConfigActivity",
|
||||
)
|
||||
for (name in configurators) {
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package de.jeanlucmakiola.clockula.domain.widget
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class StopwatchWidgetSettingsTest {
|
||||
|
||||
@Test
|
||||
fun `null parses to Card, so instances placed before styles existed keep their face`() {
|
||||
assertThat(parseStopwatchWidgetStyle(null)).isEqualTo(StopwatchWidgetStyle.CARD)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unknown name parses to Card`() {
|
||||
assertThat(parseStopwatchWidgetStyle("Nonexistent")).isEqualTo(StopwatchWidgetStyle.CARD)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `each stopwatch style's own name round-trips`() {
|
||||
StopwatchWidgetStyle.entries.forEach { style ->
|
||||
assertThat(parseStopwatchWidgetStyle(style.name)).isEqualTo(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user