neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
Add a detailed widget beside the compact one
3ead170 · Gabriel Morell · 2026-09-08 20:58
Message
{commit_body(@commit)}
Files changed
modified
clients/android/app/src/main/AndroidManifest.xml
+20
−0
@@ -70,6 +70,21 @@
| 70 | 70 | android:resource="@xml/board_widget_info" /> |
| 71 | 71 | </receiver> |
| 72 | 72 | |
| 73 | + <!-- The same board in detail, as its own widget. A home screen has | |
| 74 | + room for both, and they answer different questions: the compact | |
| 75 | + one for a glance, this one for the readings a glyph cannot | |
| 76 | + label. --> | |
| 77 | + <receiver | |
| 78 | + android:name=".widget.RichBoardWidget" | |
| 79 | + android:exported="false"> | |
| 80 | + <intent-filter> | |
| 81 | + <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> | |
| 82 | + </intent-filter> | |
| 83 | + <meta-data | |
| 84 | + android:name="android.appwidget.provider" | |
| 85 | + android:resource="@xml/rich_board_widget_info" /> | |
| 86 | + </receiver> | |
| 87 | + | |
| 73 | 88 | <!-- The cards themselves. Bound by the launcher's widget host, so it |
| 74 | 89 | needs the permission the host holds. --> |
| 75 | 90 | <service |
@@ -77,6 +92,11 @@
| 77 | 92 | android:permission="android.permission.BIND_REMOTEVIEWS" |
| 78 | 93 | android:exported="false" /> |
| 79 | 94 | |
| 95 | + <service | |
| 96 | + android:name=".widget.RichBoardWidgetService" | |
| 97 | + android:permission="android.permission.BIND_REMOTEVIEWS" | |
| 98 | + android:exported="false" /> | |
| 99 | + | |
| 80 | 100 | <!-- The board as a launcher's page to the left of the home screen. |
| 81 | 101 | Launchers implementing Google's "minus one" gesture look for this |
| 82 | 102 | action and this scheme; the api version tells the client which |
modified
clients/android/app/src/main/java/io/neiam/apolloscrib/mqtt/AnkyraService.kt
+2
−0
@@ -18,6 +18,7 @@ import kotlinx.serialization.encodeToString
| 18 | 18 | import kotlinx.serialization.json.Json |
| 19 | 19 | import io.neiam.apolloscrib.targets.Targets |
| 20 | 20 | import io.neiam.apolloscrib.widget.BoardWidget |
| 21 | +import io.neiam.apolloscrib.widget.RichBoardWidget | |
| 21 | 22 | import io.neiam.apolloscrib.ui.MainActivity |
| 22 | 23 | import kotlinx.coroutines.flow.MutableStateFlow |
| 23 | 24 | import kotlinx.coroutines.flow.StateFlow |
@@ -142,6 +143,7 @@ class AnkyraService : Service() {
| 142 | 143 | settings.clearDismissed() |
| 143 | 144 | Targets.notifyAll(this) |
| 144 | 145 | BoardWidget.refresh(this) |
| 146 | + RichBoardWidget.refresh(this) | |
| 145 | 147 | } |
| 146 | 148 | |
| 147 | 149 | private fun onState(newState: AnkyraClient.State) { |
modified
clients/android/app/src/main/java/io/neiam/apolloscrib/ui/MainActivity.kt
+2
−0
@@ -45,6 +45,7 @@ import io.neiam.apolloscrib.data.Pairing
| 45 | 45 | import io.neiam.apolloscrib.feed.FeedProvider |
| 46 | 46 | import kotlinx.coroutines.delay |
| 47 | 47 | import io.neiam.apolloscrib.widget.BoardWidget |
| 48 | +import io.neiam.apolloscrib.widget.RichBoardWidget | |
| 48 | 49 | import io.neiam.apolloscrib.data.Settings |
| 49 | 50 | import io.neiam.apolloscrib.ui.theme.ALL_THEMES |
| 50 | 51 | import io.neiam.apolloscrib.ui.theme.CribTheme |
@@ -106,6 +107,7 @@ class MainActivity : ComponentActivity() {
| 106 | 107 | // screen keeps the old colours until something |
| 107 | 108 | // unrelated happens. |
| 108 | 109 | BoardWidget.refresh(this) |
| 110 | + RichBoardWidget.refresh(this) | |
| 109 | 111 | }, |
| 110 | 112 | onConnect = { |
| 111 | 113 | AnkyraService.start(this) |
added
clients/android/app/src/main/java/io/neiam/apolloscrib/widget/RichBoardWidget.kt
+96
−0
@@ -0,0 +1,96 @@
| 1 | +package io.neiam.apolloscrib.widget | |
| 2 | + | |
| 3 | +import android.app.PendingIntent | |
| 4 | +import android.appwidget.AppWidgetManager | |
| 5 | +import android.appwidget.AppWidgetProvider | |
| 6 | +import android.content.ComponentName | |
| 7 | +import android.content.Context | |
| 8 | +import android.content.Intent | |
| 9 | +import android.net.Uri | |
| 10 | +import android.widget.RemoteViews | |
| 11 | +import io.neiam.apolloscrib.R | |
| 12 | +import io.neiam.apolloscrib.mqtt.AnkyraService | |
| 13 | +import io.neiam.apolloscrib.ui.MainActivity | |
| 14 | +import io.neiam.apolloscrib.ui.theme.appThemeByKey | |
| 15 | +import io.neiam.apolloscrib.data.Settings | |
| 16 | + | |
| 17 | +/** | |
| 18 | + * The detailed board on the home screen, a card at a time. | |
| 19 | + * | |
| 20 | + * Its own widget rather than a mode on the compact one, because a home screen | |
| 21 | + * has room for both and they answer different questions. The compact card is | |
| 22 | + * for a glance -- when is the next one -- and this is for the readings a | |
| 23 | + * glyph cannot label: pressure, visibility, which tide and how long. | |
| 24 | + * | |
| 25 | + * Both draw the same board. This one draws it through | |
| 26 | + * [io.neiam.apolloscrib.ui.factsFor], the same reading the app's detailed mode | |
| 27 | + * uses, so a card says the same thing on a home screen as it does in the app. | |
| 28 | + */ | |
| 29 | +class RichBoardWidget : AppWidgetProvider() { | |
| 30 | + | |
| 31 | + override fun onUpdate( | |
| 32 | + context: Context, | |
| 33 | + manager: AppWidgetManager, | |
| 34 | + widgetIds: IntArray | |
| 35 | + ) { | |
| 36 | + widgetIds.forEach { id -> manager.updateAppWidget(id, build(context, id)) } | |
| 37 | + } | |
| 38 | + | |
| 39 | + private fun build(context: Context, widgetId: Int): RemoteViews { | |
| 40 | + val palette = appThemeByKey(Settings(context).themeKey) | |
| 41 | + | |
| 42 | + return RemoteViews(context.packageName, R.layout.widget_rich_board).apply { | |
| 43 | + setInt(R.id.rich_board_root, "setBackgroundColor", palette.bg.toArgb()) | |
| 44 | + setTextColor(R.id.rich_board_empty, palette.dim.toArgb()) | |
| 45 | + | |
| 46 | + // The factory needs the widget id, and an Intent's extras are not | |
| 47 | + // part of its identity -- two widgets would share one factory | |
| 48 | + // unless the data URI differs. | |
| 49 | + val data = Intent(context, RichBoardWidgetService::class.java).apply { | |
| 50 | + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) | |
| 51 | + setData(Uri.parse("apolloscrib://richwidget/$widgetId")) | |
| 52 | + } | |
| 53 | + setRemoteAdapter(R.id.rich_flipper, data) | |
| 54 | + setEmptyView(R.id.rich_flipper, R.id.rich_board_empty) | |
| 55 | + | |
| 56 | + // Tapping any card opens the app. Read-only, like every other | |
| 57 | + // surface: there is nothing on a card to act on. | |
| 58 | + val open = PendingIntent.getActivity( | |
| 59 | + context, | |
| 60 | + widgetId, | |
| 61 | + Intent(context, MainActivity::class.java), | |
| 62 | + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT | |
| 63 | + ) | |
| 64 | + setPendingIntentTemplate(R.id.rich_flipper, open) | |
| 65 | + } | |
| 66 | + } | |
| 67 | + | |
| 68 | + override fun onEnabled(context: Context) { | |
| 69 | + // A widget on the home screen is a reason to be connected, the same | |
| 70 | + // way the feed page is. | |
| 71 | + AnkyraService.resume(context) | |
| 72 | + } | |
| 73 | + | |
| 74 | + companion object { | |
| 75 | + | |
| 76 | + /** | |
| 77 | + * Redraw every widget from what just arrived. | |
| 78 | + * | |
| 79 | + * Two calls: the data change reloads the factory, and the update | |
| 80 | + * rebuilds the flipper around it. Without the second, a widget added | |
| 81 | + * before the first board keeps showing its empty view. | |
| 82 | + */ | |
| 83 | + fun refresh(context: Context) { | |
| 84 | + val manager = AppWidgetManager.getInstance(context) ?: return | |
| 85 | + val ids = manager.getAppWidgetIds(ComponentName(context, RichBoardWidget::class.java)) | |
| 86 | + if (ids.isEmpty()) return | |
| 87 | + manager.notifyAppWidgetViewDataChanged(ids, R.id.rich_flipper) | |
| 88 | + context.sendBroadcast( | |
| 89 | + Intent(context, RichBoardWidget::class.java).apply { | |
| 90 | + action = AppWidgetManager.ACTION_APPWIDGET_UPDATE | |
| 91 | + putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids) | |
| 92 | + } | |
| 93 | + ) | |
| 94 | + } | |
| 95 | + } | |
| 96 | +} |
added
clients/android/app/src/main/java/io/neiam/apolloscrib/widget/RichBoardWidgetService.kt
+117
−0
@@ -0,0 +1,117 @@
| 1 | +package io.neiam.apolloscrib.widget | |
| 2 | + | |
| 3 | +import android.content.Context | |
| 4 | +import android.content.Intent | |
| 5 | +import android.widget.RemoteViews | |
| 6 | +import android.widget.RemoteViewsService | |
| 7 | +import io.neiam.apolloscrib.R | |
| 8 | +import io.neiam.apolloscrib.data.Settings | |
| 9 | +import io.neiam.apolloscrib.data.VisionStore | |
| 10 | +import io.neiam.apolloscrib.types.SourceType | |
| 11 | +import io.neiam.apolloscrib.ui.RichFacts | |
| 12 | +import io.neiam.apolloscrib.ui.factsFor | |
| 13 | +import io.neiam.apolloscrib.ui.theme.AppTheme | |
| 14 | +import io.neiam.apolloscrib.ui.theme.appThemeByKey | |
| 15 | + | |
| 16 | +/** | |
| 17 | + * The cards behind the detailed widget. | |
| 18 | + * | |
| 19 | + * Built from [factsFor], which is the same reading the app's detailed mode | |
| 20 | + * uses — a headline that answers the question and beneath it the things that | |
| 21 | + * qualify it, each saying its own name. That function has no Compose in it, | |
| 22 | + * which is what lets a RemoteViews surface share it rather than growing a | |
| 23 | + * second opinion about what a weather card should say. | |
| 24 | + */ | |
| 25 | +class RichBoardWidgetService : RemoteViewsService() { | |
| 26 | + | |
| 27 | + override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = | |
| 28 | + Factory(applicationContext) | |
| 29 | + | |
| 30 | + private class Factory(private val context: Context) : RemoteViewsFactory { | |
| 31 | + | |
| 32 | + private val store = VisionStore(context) | |
| 33 | + private var cards: List<RichFacts> = emptyList() | |
| 34 | + private var palette: AppTheme = appThemeByKey(Settings(context).themeKey) | |
| 35 | + | |
| 36 | + override fun onCreate() = Unit | |
| 37 | + | |
| 38 | + override fun onDataSetChanged() { | |
| 39 | + palette = appThemeByKey(Settings(context).themeKey) | |
| 40 | + | |
| 41 | + // The Plus board where there is one, as the compact widget does: | |
| 42 | + // the same board with more on it, and Basic's own answer for every | |
| 43 | + // type Plus does not extend. | |
| 44 | + val board = store.plusEntries().ifEmpty { store.entries() } | |
| 45 | + | |
| 46 | + // Transit is left to the compact widget. Its detailed card is a | |
| 47 | + // route badge and a list of departures each with its own delay and | |
| 48 | + // crowding, which is a scrolling thing, and a widget card does not | |
| 49 | + // scroll -- it would come out truncated in a way that hides the | |
| 50 | + // very detail it is here for. | |
| 51 | + cards = board | |
| 52 | + .filter { it.type != SourceType.Gtfs && it.type != SourceType.GtfsPlus } | |
| 53 | + .mapNotNull { factsFor(it) } | |
| 54 | + } | |
| 55 | + | |
| 56 | + override fun onDestroy() { | |
| 57 | + cards = emptyList() | |
| 58 | + } | |
| 59 | + | |
| 60 | + override fun getCount(): Int = cards.size | |
| 61 | + | |
| 62 | + override fun getViewAt(position: Int): RemoteViews { | |
| 63 | + val card = cards.getOrNull(position) ?: return loading() | |
| 64 | + | |
| 65 | + return RemoteViews(context.packageName, R.layout.widget_rich_card).apply { | |
| 66 | + setInt(R.id.rich_root, "setBackgroundColor", palette.cardBg.toArgb()) | |
| 67 | + | |
| 68 | + setImageViewResource(R.id.rich_icon, card.iconRes) | |
| 69 | + setInt(R.id.rich_icon, "setColorFilter", palette.primary.toArgb()) | |
| 70 | + | |
| 71 | + setTextViewText(R.id.rich_title, card.title) | |
| 72 | + setTextColor(R.id.rich_title, palette.content.toArgb()) | |
| 73 | + | |
| 74 | + setTextViewText(R.id.rich_headline, card.headline.orEmpty()) | |
| 75 | + setTextColor(R.id.rich_headline, palette.accent.toArgb()) | |
| 76 | + setViewVisibility( | |
| 77 | + R.id.rich_headline, | |
| 78 | + if (card.headline == null) android.view.View.GONE else android.view.View.VISIBLE | |
| 79 | + ) | |
| 80 | + | |
| 81 | + setTextViewText(R.id.rich_caption, card.caption.orEmpty()) | |
| 82 | + setTextColor(R.id.rich_caption, palette.dim.toArgb()) | |
| 83 | + setViewVisibility( | |
| 84 | + R.id.rich_caption, | |
| 85 | + if (card.caption == null) android.view.View.GONE else android.view.View.VISIBLE | |
| 86 | + ) | |
| 87 | + | |
| 88 | + removeAllViews(R.id.rich_facts) | |
| 89 | + | |
| 90 | + // Four, because a widget card does not scroll and a fifth row | |
| 91 | + // would be drawn off the bottom of it -- present, unreadable, | |
| 92 | + // and indistinguishable from a card that simply ended. | |
| 93 | + card.facts.take(4).forEach { fact -> | |
| 94 | + addView( | |
| 95 | + R.id.rich_facts, | |
| 96 | + RemoteViews(context.packageName, R.layout.widget_fact).apply { | |
| 97 | + setTextViewText(R.id.fact_label, fact.label) | |
| 98 | + setTextColor(R.id.fact_label, palette.dim.toArgb()) | |
| 99 | + setTextViewText(R.id.fact_value, fact.value) | |
| 100 | + setTextColor(R.id.fact_value, palette.content.toArgb()) | |
| 101 | + } | |
| 102 | + ) | |
| 103 | + } | |
| 104 | + } | |
| 105 | + } | |
| 106 | + | |
| 107 | + private fun loading() = RemoteViews(context.packageName, R.layout.widget_rich_card) | |
| 108 | + | |
| 109 | + override fun getLoadingView(): RemoteViews? = null | |
| 110 | + | |
| 111 | + override fun getViewTypeCount(): Int = 1 | |
| 112 | + | |
| 113 | + override fun getItemId(position: Int): Long = position.toLong() | |
| 114 | + | |
| 115 | + override fun hasStableIds(): Boolean = true | |
| 116 | + } | |
| 117 | +} |
added
clients/android/app/src/main/res/layout/widget_fact.xml
+28
−0
@@ -0,0 +1,28 @@
| 1 | +<?xml version="1.0" encoding="utf-8"?> | |
| 2 | +<!-- A named value. The label carries the meaning the glyph carries on a | |
| 3 | + compact row, which is the whole reason this layout exists. --> | |
| 4 | +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
| 5 | + android:layout_width="match_parent" | |
| 6 | + android:layout_height="wrap_content" | |
| 7 | + android:orientation="horizontal" | |
| 8 | + android:paddingTop="2dp" | |
| 9 | + android:paddingBottom="2dp"> | |
| 10 | + | |
| 11 | + <TextView | |
| 12 | + android:id="@+id/fact_label" | |
| 13 | + android:layout_width="0dp" | |
| 14 | + android:layout_height="wrap_content" | |
| 15 | + android:layout_weight="1" | |
| 16 | + android:maxLines="1" | |
| 17 | + android:ellipsize="end" | |
| 18 | + android:textSize="12sp" /> | |
| 19 | + | |
| 20 | + <TextView | |
| 21 | + android:id="@+id/fact_value" | |
| 22 | + android:layout_width="wrap_content" | |
| 23 | + android:layout_height="wrap_content" | |
| 24 | + android:layout_marginStart="10dp" | |
| 25 | + android:maxLines="1" | |
| 26 | + android:textSize="13sp" | |
| 27 | + android:textStyle="bold" /> | |
| 28 | +</LinearLayout> |
added
clients/android/app/src/main/res/layout/widget_rich_board.xml
+27
−0
@@ -0,0 +1,27 @@
| 1 | +<?xml version="1.0" encoding="utf-8"?> | |
| 2 | +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
| 3 | + android:id="@+id/rich_board_root" | |
| 4 | + android:layout_width="match_parent" | |
| 5 | + android:layout_height="match_parent" | |
| 6 | + android:padding="2dp"> | |
| 7 | + | |
| 8 | + <AdapterViewFlipper | |
| 9 | + android:id="@+id/rich_flipper" | |
| 10 | + android:layout_width="match_parent" | |
| 11 | + android:layout_height="match_parent" | |
| 12 | + android:loopViews="true" | |
| 13 | + android:autoStart="true" | |
| 14 | + android:flipInterval="8000" | |
| 15 | + android:animateFirstView="false" /> | |
| 16 | + | |
| 17 | + <!-- Shown by the flipper itself when the board has nothing in it, so the | |
| 18 | + widget says why rather than sitting blank. --> | |
| 19 | + <TextView | |
| 20 | + android:id="@+id/rich_board_empty" | |
| 21 | + android:layout_width="match_parent" | |
| 22 | + android:layout_height="match_parent" | |
| 23 | + android:gravity="center" | |
| 24 | + android:padding="12dp" | |
| 25 | + android:textSize="13sp" | |
| 26 | + android:text="@string/widget_empty" /> | |
| 27 | +</FrameLayout> |
added
clients/android/app/src/main/res/layout/widget_rich_card.xml
+63
−0
@@ -0,0 +1,63 @@
| 1 | +<?xml version="1.0" encoding="utf-8"?> | |
| 2 | +<!-- One card in the detailed widget: the thing being asked about, large, and | |
| 3 | + beneath it the things that qualify it, each saying its own name. | |
| 4 | + | |
| 5 | + The compact card is a glyph and a number against the right edge, which | |
| 6 | + works because you know what a droplet means. This is for the readings | |
| 7 | + where you do not: pressure, visibility, how long until the tide. --> | |
| 8 | +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
| 9 | + android:id="@+id/rich_root" | |
| 10 | + android:layout_width="match_parent" | |
| 11 | + android:layout_height="match_parent" | |
| 12 | + android:orientation="vertical" | |
| 13 | + android:padding="14dp"> | |
| 14 | + | |
| 15 | + <LinearLayout | |
| 16 | + android:layout_width="match_parent" | |
| 17 | + android:layout_height="wrap_content" | |
| 18 | + android:orientation="horizontal" | |
| 19 | + android:gravity="center_vertical"> | |
| 20 | + | |
| 21 | + <ImageView | |
| 22 | + android:id="@+id/rich_icon" | |
| 23 | + android:layout_width="18dp" | |
| 24 | + android:layout_height="18dp" | |
| 25 | + android:layout_marginEnd="8dp" | |
| 26 | + android:contentDescription="@null" /> | |
| 27 | + | |
| 28 | + <TextView | |
| 29 | + android:id="@+id/rich_title" | |
| 30 | + android:layout_width="match_parent" | |
| 31 | + android:layout_height="wrap_content" | |
| 32 | + android:maxLines="1" | |
| 33 | + android:ellipsize="end" | |
| 34 | + android:textSize="13sp" | |
| 35 | + android:textStyle="bold" /> | |
| 36 | + </LinearLayout> | |
| 37 | + | |
| 38 | + <TextView | |
| 39 | + android:id="@+id/rich_headline" | |
| 40 | + android:layout_width="match_parent" | |
| 41 | + android:layout_height="wrap_content" | |
| 42 | + android:layout_marginTop="6dp" | |
| 43 | + android:maxLines="1" | |
| 44 | + android:ellipsize="end" | |
| 45 | + android:textSize="30sp" /> | |
| 46 | + | |
| 47 | + <TextView | |
| 48 | + android:id="@+id/rich_caption" | |
| 49 | + android:layout_width="match_parent" | |
| 50 | + android:layout_height="wrap_content" | |
| 51 | + android:maxLines="2" | |
| 52 | + android:ellipsize="end" | |
| 53 | + android:textSize="12sp" /> | |
| 54 | + | |
| 55 | + <!-- Filled at bind time, one row per fact. Empty for a card that has a | |
| 56 | + headline and nothing to qualify it, which is a whole answer. --> | |
| 57 | + <LinearLayout | |
| 58 | + android:id="@+id/rich_facts" | |
| 59 | + android:layout_width="match_parent" | |
| 60 | + android:layout_height="wrap_content" | |
| 61 | + android:layout_marginTop="8dp" | |
| 62 | + android:orientation="vertical" /> | |
| 63 | +</LinearLayout> |
modified
clients/android/app/src/main/res/values/strings.xml
+1
−0
@@ -3,5 +3,6 @@
| 3 | 3 | <string name="app_name">Apollo\'s Crib</string> |
| 4 | 4 | <string name="configure">Choose a query</string> |
| 5 | 5 | <string name="widget_description">A vision\'s board, one card at a time</string> |
| 6 | + <string name="rich_widget_description">A vision\'s board in detail, one card at a time</string> | |
| 6 | 7 | <string name="widget_empty">No board yet — open Apollo\'s Crib to connect</string> |
| 7 | 8 | </resources> |
added
clients/android/app/src/main/res/xml/rich_board_widget_info.xml
+30
−0
@@ -0,0 +1,30 @@
| 1 | +<?xml version="1.0" encoding="utf-8"?> | |
| 2 | +<!-- The detailed board as a home screen widget: one card at a time, cycled | |
| 3 | + by the host. | |
| 4 | + | |
| 5 | + Larger than its compact sibling by default, because that is the point of | |
| 6 | + it — a headline and a handful of named values need room that four rows | |
| 7 | + of glyph-and-number do not. | |
| 8 | + | |
| 9 | + `autoAdvanceViewId` asks for the cycling: the flipper advances itself on | |
| 10 | + launchers that honour it, and can be swiped by hand on the ones that do | |
| 11 | + not. | |
| 12 | + | |
| 13 | + No `updatePeriodMillis`: a board arrives when a Pythiae publishes one, and | |
| 14 | + the service notifies the widget then. --> | |
| 15 | +<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" | |
| 16 | + android:minWidth="250dp" | |
| 17 | + android:minHeight="180dp" | |
| 18 | + android:minResizeWidth="110dp" | |
| 19 | + android:minResizeHeight="70dp" | |
| 20 | + android:targetCellWidth="4" | |
| 21 | + android:targetCellHeight="3" | |
| 22 | + android:maxResizeWidth="500dp" | |
| 23 | + android:maxResizeHeight="400dp" | |
| 24 | + android:resizeMode="horizontal|vertical" | |
| 25 | + android:updatePeriodMillis="0" | |
| 26 | + android:initialLayout="@layout/widget_rich_board" | |
| 27 | + android:previewLayout="@layout/widget_rich_board" | |
| 28 | + android:autoAdvanceViewId="@id/rich_flipper" | |
| 29 | + android:widgetCategory="home_screen" | |
| 30 | + android:description="@string/rich_widget_description" /> |
Parents: 48d3a04