@@ -19,6 +19,8 @@ import com.mg4.hardware.FirmwareGen
1919import com.mg4.hardware.FirmwareSupport
2020import com.mg4.tasker.store.SupportStore
2121import com.mg4.tasker.util.BtDevices
22+ import com.mg4.tasker.util.EmulatorDetector
23+ import com.mg4.tasker.util.MapApps
2224import com.mg4.tasker.util.SpeechEngines
2325import com.mg4.tasker.vehicle.ProfileBridge
2426import java.util.Locale
@@ -46,12 +48,19 @@ object CatalogPicker {
4648
4749 fun pickCondition (context : Context , firmware : FirmwareGen ? , onPick : (ConditionType ) -> Unit ) {
4850 val allowed = SupportStore .supportedConditions(context)
51+ val diagnosticBlocked = SupportStore .diagnosticBlockedConditions(context)
52+ // No emulator image has a paired device, a diagnostic history, or vendor services —
53+ // on the head unit those gaps mean "would never work", on an emulator they just mean
54+ // "nobody ran the prerequisite step yet". Skip them so every entry stays pickable to
55+ // debug, whichever hardware read behind it ends up failing on its own.
56+ val debug = EmulatorDetector .isEmulator()
4957 // Nothing paired means the device picker would be empty, and the rule would carry a
5058 // blank MAC that can never match. Same reasoning as the profile action below.
5159 val hasPairedDevice = BtDevices .bonded(context).isNotEmpty()
5260 val groups = ConditionType .byGroup().mapNotNull { (group, types) ->
5361 val supported = types
54- .filter { hasPairedDevice || it.spec.kind != ValueKind .BT_DEVICE }
62+ .filter { debug || hasPairedDevice || it.spec.kind != ValueKind .BT_DEVICE }
63+ .filter { debug || it.name !in diagnosticBlocked }
5564 .filter { type ->
5665 allow(allowed, type.name) { FirmwareSupport .isSupported(type, firmware) }
5766 }
@@ -68,21 +77,25 @@ object CatalogPicker {
6877
6978 fun pickAction (context : Context , firmware : FirmwareGen ? , onPick : (ActionType ) -> Unit ) {
7079 val allowed = SupportStore .supportedActions(context)
80+ val diagnosticBlocked = SupportStore .diagnosticBlockedActions(context)
81+ // See pickCondition: an emulator fails every local prerequisite by construction, so
82+ // none of them should hide an entry there.
83+ val debug = EmulatorDetector .isEmulator()
7184 // The profile actions are the ones that cannot work without MG4Control. Offering them
7285 // with an empty profile list only produces a rule that fails at ignition.
7386 val hasProfiles = ProfileBridge .isMG4ControlInstalled(context)
7487 val visible = ActionType .entries.filterNot { it in CatalogVisibility .hiddenActions }
7588 val grouped = visible.groupBy { type ->
7689 when (type) {
7790 ActionType .APPLY_PROFILE , ActionType .SHOW_PROFILE_PICKER ,
78- ActionType .WEBHOOK_GET , ActionType . WEBHOOK_POST -> IntegrationGroup
91+ ActionType .WEBHOOK -> IntegrationGroup
7992 else -> type.group
8093 }
8194 }
8295 val groups = grouped.mapNotNull { (group, types) ->
8396 val supported = types
84- .filter { hasProfiles || it.group != ActionGroup .PROFILE }
85- .filter { runnableHere(context, it ) }
97+ .filter { debug || hasProfiles || it.group != ActionGroup .PROFILE }
98+ .filter { type -> debug || runnableHere(context, type) ? : (type.name !in diagnosticBlocked ) }
8699 .filter { type -> allow(allowed, type.name) { FirmwareSupport .isSupported(type, firmware) } }
87100 if (supported.isEmpty()) return @mapNotNull null
88101 Group (
@@ -117,20 +130,27 @@ object CatalogPicker {
117130 allowed?.contains(name) ? : live()
118131
119132 /* *
120- * The local actions depend on the head unit, not on the firmware matrix, so the matrix
121- * cannot filter them.
133+ * Answers "can this head unit do it" for the actions that can be checked **now**, or null
134+ * when only the last diagnostic run can say.
135+ *
136+ * These depend on the head unit rather than on the firmware matrix, so the matrix cannot
137+ * filter them; and they are cheap local queries — a package lookup, no vehicle read,
138+ * nothing that blocks — so they are asked live rather than read back from a stored
139+ * diagnostic. That matters when the answer changes: installing a speech engine or a map
140+ * app brings its action back to the picker immediately, without a second diagnostic.
122141 *
123142 * Speech was the visible case: the Diagnostic tab reported "no speech engine" while the
124143 * editor kept offering the action, and picking it produced a rule that could only ever
125- * fail. Both now ask [SpeechEngines] the same question — a package query, no vehicle
126- * read, nothing that blocks.
144+ * fail. Both now ask [SpeechEngines] the same question.
127145 *
128- * The message action is not filtered : it shows on screen whatever the notification
146+ * The message action is deliberately absent : it shows on screen whatever the notification
129147 * channel is doing, so it always has a way to reach the driver.
130148 */
131- private fun runnableHere (context : Context , type : ActionType ): Boolean = when (type) {
149+ private fun runnableHere (context : Context , type : ActionType ): Boolean? = when (type) {
132150 ActionType .SPEAK_TEXT -> SpeechEngines .any(context)
133- else -> true
151+ ActionType .NAVIGATE_TO -> MapApps .isAvailable(context)
152+ ActionType .SHOW_NOTIFICATION -> true
153+ else -> null
134154 }
135155
136156 private class Group (val label : String , val entries : List <Entry >)
0 commit comments