Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,16 @@ gives the timestamped jar name; a CI run started before that timestamp cannot se
with no modules (list collapsed to "ECU"). Requests now wait in the queue; `onDTCCompleted` uses the finished
request's kind / modules and holds back `DATA_LOGGER_DTC_ACTION_COMPLETED` while another one waits. Logcat:
"DTC action completed: <kind>, ... next=<kind>". Any new DTC entry point must go through `submit()`.
* **A read / clear of some modules updates only those** (`dtc/DtcStoredList.kt`, spec `doc/specs/dtc-clear-module.md`).
The clear's follow-up read covers the cleared modules alone, so `updateDTC` replacing the list made the other modules'
codes vanish. `mergedOver` keeps the untargeted modules' codes / statuses for READ / CLEAR of the same known vehicle;
CONNECT replaces (a module removed from the table must not linger). History records the read alone. The phone's
"Clear this module" button (`DtcListItem.ModuleClear`, right under the header: the user wants it at the top, and shown
even for the only module with codes - hiding it there looked like the button was gone) needs `dtcModuleRequestKey`: codes carry the display
label, `scheduleDTCCleanup` takes table `requestKey`s.
* Engine-running check before a clear: `EngineStateTracker` (registered on `MetricsObserver`) keeps the last RPM /
vehicle-status reading; `engineRunning()` is null when older than 10 s or never seen. It only warns — the running
query may have no engine PID, so null must never block.
* **New-code notification** (`dtc/DtcNewCodes.kt`, spec `doc/specs/dtc-new-code-notification.md`): `updateDTC` ends
with `DtcNewCodesAnnouncer.announce`, so every connect path is covered; only `CONNECT` scans with a VIN reply; all
codes on the vehicle's first scan, else only `diff.isNew` ones. "First scan" is read before `DtcHistoryStore.append`:
Expand All @@ -391,6 +401,8 @@ gives the timestamped jar name; a CI run started before that timestamp cannot se
another code-0 launch intent replaces the extras. Open a screen from outside via `EXTRA_NAVIGATE_TO` (`:common`) +
`navigateToPreferencesScreen(key, openDialog = true)`.
* String resources drop a leading space: join text parts in code (`DtcReadInfo.summary`).
* `dialog_close_window_action` is "Close\n " on purpose (two lines, to match the trip dialog's two-line buttons); next
to one-line buttons its label sits a line high. The DTC dialog uses its own `dtc.action_close`.
* Building ObdMetrics: `mvn` needs JDK 11 (`JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64`); the
delombok plugin fails on JDK 21. Its test helpers (`SimpleLifecycle`, ...) live in the separate
`ObdMetricsTest` repo and do not capture the clear status, so a test needs its own `Lifecycle`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.obd.graphs.preferences.dtc

import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.text.format.DateUtils
Expand Down Expand Up @@ -45,10 +46,12 @@ import org.obd.graphs.bl.datalogger.dataLoggerSettings
import org.obd.graphs.bl.datalogger.dtc.DtcClearOutcome
import org.obd.graphs.bl.datalogger.dtc.DtcHistoryStore
import org.obd.graphs.bl.datalogger.dtc.DtcReadInfoLabels
import org.obd.graphs.bl.datalogger.dtc.EngineStateTracker
import org.obd.graphs.bl.datalogger.dtc.PREF_DTC_DESELECTED_MODULES
import org.obd.graphs.bl.datalogger.dtc.collapsedBy
import org.obd.graphs.bl.datalogger.dtc.displayCode
import org.obd.graphs.bl.datalogger.dtc.dtcActionTimeoutMs
import org.obd.graphs.bl.datalogger.dtc.dtcModuleRequestKey
import org.obd.graphs.bl.datalogger.dtc.dtcStatus
import org.obd.graphs.bl.datalogger.dtc.markedWith
import org.obd.graphs.bl.datalogger.dtc.sortedForDisplay
Expand All @@ -58,6 +61,7 @@ import org.obd.graphs.bl.datalogger.dtc.toDidLines
import org.obd.graphs.bl.datalogger.dtc.toDtcListItems
import org.obd.graphs.bl.datalogger.dtc.unansweredModules
import org.obd.graphs.bl.datalogger.dtc.withGoneSection
import org.obd.graphs.bl.datalogger.dtc.withModuleClearActions
import org.obd.graphs.bl.datalogger.dtc.withModuleSummaries
import org.obd.graphs.bl.readiness.PREF_READINESS_LAST
import org.obd.graphs.bl.readiness.ReadinessSnapshot
Expand Down Expand Up @@ -136,7 +140,8 @@ internal class DiagnosticTroubleCodePreferenceDialogFragment : CoreDialogFragmen
onModuleHeaderClick = { header ->
moduleExpanded[header.module] = !header.expanded
refreshList()
}
},
onModuleClearClick = { module -> clearModule(module) }
)
recyclerView.layoutManager = GridLayoutManager(context, 1)
recyclerView.adapter = adapter
Expand Down Expand Up @@ -190,26 +195,67 @@ internal class DiagnosticTroubleCodePreferenceDialogFragment : CoreDialogFragmen

clearButton.setOnClickListener {
if (DataLoggerRepository.isRunning()) {
pickDtcModules(
titleRes = R.string.pref_dtc_clean_dialog_title,
confirmRes = R.string.pref_dtc_select_modules_confirm_clear,
messageRes = R.string.pref_dtc_clean_dialog_confirm_message,
confirmWhenEmpty = true
) { selectedModules ->
setLoadingState(true, selectedModules.size)
withDataLogger {
scheduleDTCCleanup(selectedModules)
}

toast(R.string.pref_dtc_clean_dialog_send_message)
clearButton.setText(R.string.dtc_action_clearing)
confirmEngineOff {
pickDtcModules(
titleRes = R.string.pref_dtc_clean_dialog_title,
confirmRes = R.string.pref_dtc_select_modules_confirm_clear,
messageRes = R.string.pref_dtc_clean_dialog_confirm_message,
confirmWhenEmpty = true
) { selectedModules -> startClear(selectedModules) }
}
} else {
toast(R.string.pref_dtc_no_connection_established)
}
}
}

private fun startClear(modules: Set<String>) {
setLoadingState(true, modules.size)
withDataLogger {
scheduleDTCCleanup(modules)
}

toast(R.string.pref_dtc_clean_dialog_send_message)
clearButton.setText(R.string.dtc_action_clearing)
}

// A recent reading says the engine runs: most modules would refuse the clear (7F 14 22), so say so first and let
// the user go on anyway. Unknown (no engine PID in the running query) goes straight on.
private fun confirmEngineOff(onConfirmed: () -> Unit) {
if (EngineStateTracker.engineRunning() != true) {
onConfirmed()
return
}
AlertDialog
.Builder(requireContext())
.setTitle(R.string.dtc_engine_running_title)
.setMessage(R.string.dtc_engine_running_message)
.setPositiveButton(R.string.dtc_engine_running_clear_anyway) { _, _ -> onConfirmed() }
.setNegativeButton(R.string.pref_dtc_select_modules_cancel, null)
.show()
}

// "Clear codes in <module>": one module, one confirmation (with the engine warning folded in), no picker.
private fun clearModule(module: String) {
if (!DataLoggerRepository.isRunning()) {
toast(R.string.pref_dtc_no_connection_established)
return
}
val requestKey = dtcModuleRequestKey(DiagnosticRequestIDManager.getMappings(), module) ?: return
val message =
listOfNotNull(
getString(R.string.dtc_engine_running_message).takeIf { EngineStateTracker.engineRunning() == true },
getString(R.string.dtc_module_clear_message)
).joinToString("\n\n")
AlertDialog
.Builder(requireContext())
.setTitle(getString(R.string.dtc_module_clear_title, module))
.setMessage(message)
.setPositiveButton(R.string.dtc_module_clear_confirm) { _, _ -> startClear(setOf(requestKey)) }
.setNegativeButton(R.string.pref_dtc_select_modules_cancel, null)
.show()
}

// Lets the user restrict a DTC read/clear to a subset of the configured Diagnostic Request ID
// modules for this action only (not persisted), folding in an optional message (a plain
// description for the Scan flow, a destructive-action warning for the Clear flow) so a single
Expand Down Expand Up @@ -469,6 +515,7 @@ internal class DiagnosticTroubleCodePreferenceDialogFragment : CoreDialogFragmen

private fun refreshList() {
val codes = diagnosticTroubleCodes()
val mappings = DiagnosticRequestIDManager.getMappings()
// New / gone markers from the vehicle's DTC history, for the list stored with this read info.
val history = DtcHistoryStore.diff(VehicleCapabilitiesManager.getDtcReadInfo())
adapter.submitList(
Expand All @@ -484,6 +531,7 @@ internal class DiagnosticTroubleCodePreferenceDialogFragment : CoreDialogFragmen
).markedWith(history)
.withModuleSummaries()
.collapsedBy { moduleExpanded[it] }
.withModuleClearActions { dtcModuleRequestKey(mappings, it) != null }
.withGoneSection(history, goneExpanded)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ private const val VIEW_TYPE_HEADER = 0
private const val VIEW_TYPE_DTC = 1
private const val VIEW_TYPE_MESSAGE = 2
private const val VIEW_TYPE_GONE_HEADER = 3
private const val VIEW_TYPE_MODULE_CLEAR = 4
private const val GONE_ROW_ALPHA = 0.6f
private const val BADGE_FILL_ALPHA = 0x33

Expand Down Expand Up @@ -100,7 +101,7 @@ internal fun showDtcHistoryHelp(context: Context) {
AlertDialog.Builder(context)
.setTitle(R.string.dtc_history_help_title)
.setMessage(context.getText(R.string.dtc_history_help_body))
.setPositiveButton(R.string.dialog_close_window_action, null)
.setPositiveButton(R.string.dtc_action_close, null)
.show()
}

Expand Down Expand Up @@ -168,10 +169,12 @@ internal fun TextView.showBadge(
}

// onGoneHeaderClick / onModuleHeaderClick: a section header was tapped; the owner rebuilds the list expanded / collapsed.
// onModuleClearClick: "Clear codes in <module>" was tapped; the owner confirms and clears.
internal class DiagnosticTroubleCodeViewAdapter internal constructor(
context: Context?,
private val onGoneHeaderClick: () -> Unit = {},
private val onModuleHeaderClick: (DtcListItem.ModuleHeader) -> Unit = {}
private val onModuleHeaderClick: (DtcListItem.ModuleHeader) -> Unit = {},
private val onModuleClearClick: (String) -> Unit = {}
) : ListAdapter<DtcListItem, RecyclerView.ViewHolder>(DiffCallback) {
private val mInflater: LayoutInflater = LayoutInflater.from(context)

Expand All @@ -185,6 +188,7 @@ internal class DiagnosticTroubleCodeViewAdapter internal constructor(
is DtcListItem.DtcRow -> VIEW_TYPE_DTC
is DtcListItem.Message -> VIEW_TYPE_MESSAGE
is DtcListItem.GoneHeader -> VIEW_TYPE_GONE_HEADER
is DtcListItem.ModuleClear -> VIEW_TYPE_MODULE_CLEAR
}

override fun onCreateViewHolder(
Expand All @@ -195,6 +199,7 @@ internal class DiagnosticTroubleCodeViewAdapter internal constructor(
VIEW_TYPE_HEADER -> HeaderViewHolder(mInflater.inflate(R.layout.item_dtc_module_header, parent, false))
VIEW_TYPE_MESSAGE -> MessageViewHolder(mInflater.inflate(R.layout.item_dtc_message, parent, false))
VIEW_TYPE_GONE_HEADER -> GoneHeaderViewHolder(mInflater.inflate(R.layout.item_dtc_module_header, parent, false))
VIEW_TYPE_MODULE_CLEAR -> ModuleClearViewHolder(mInflater.inflate(R.layout.item_dtc_module_clear, parent, false))
else -> DtcViewHolder(mInflater.inflate(R.layout.item_dtc, parent, false))
}

Expand All @@ -207,6 +212,7 @@ internal class DiagnosticTroubleCodeViewAdapter internal constructor(
is DtcListItem.DtcRow -> bindDtc(holder as DtcViewHolder, item)
is DtcListItem.Message -> (holder as MessageViewHolder).bind(item)
is DtcListItem.GoneHeader -> (holder as GoneHeaderViewHolder).bind(item, onGoneHeaderClick)
is DtcListItem.ModuleClear -> (holder as ModuleClearViewHolder).bind(item, onModuleClearClick)
}
}

Expand Down Expand Up @@ -468,6 +474,19 @@ internal class DiagnosticTroubleCodeViewAdapter internal constructor(
}
}

internal class ModuleClearViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val action: TextView = itemView.findViewById(R.id.dtc_module_clear)

fun bind(
item: DtcListItem.ModuleClear,
onClick: (String) -> Unit
) {
// The header above names the module; a screen reader reads the row alone, so it gets the name too.
action.contentDescription = "${action.text} (${item.module})"
action.setOnClickListener { onClick(item.module) }
}
}

internal class MessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val text: TextView = itemView.findViewById(R.id.dtc_message)

Expand Down
11 changes: 11 additions & 0 deletions app/src/main/res/drawable/dtc_clear_module.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- "Clear this module" button under a module header: a bin (Material "delete_outline"). -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM8,9h8v10H8V9zM15.5,4l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>
18 changes: 18 additions & 0 deletions app/src/main/res/drawable/dtc_module_clear_background.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Outlined pill of the "Clear this module" button: red outline, no fill, so it reads as an action and not as a code
(codes are red text). Inset so the pill is ~32dp tall inside a 48dp touch target. -->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:attr/colorControlHighlight">
<item>
<inset
android:insetTop="8dp"
android:insetBottom="8dp">
<shape android:shape="rectangle">
<corners android:radius="16dp" />
<stroke
android:width="1dp"
android:color="@color/cardinal" />
</shape>
</inset>
</item>
</ripple>
2 changes: 1 addition & 1 deletion app/src/main/res/layout/dialog_dtc.xml
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@
android:layout_weight="1"
android:layout_marginStart="4dp"
android:backgroundTint="@color/rainbow_indigo"
android:text="@string/dialog_close_window_action"
android:text="@string/dtc.action_close"
android:textSize="12sp" />
</LinearLayout>

Expand Down
29 changes: 29 additions & 0 deletions app/src/main/res/layout/item_dtc_module_clear.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- "Clear this module" right under an expanded module's header (DtcListItem.ModuleClear). A small outlined button
aligned with the header text, red only in its outline and icon: the codes are red text, and the action must not
look like one more of them. -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-6dp"
android:paddingStart="8dp"
android:paddingEnd="8dp">

<TextView
android:id="@+id/dtc_module_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:background="@drawable/dtc_module_clear_background"
android:drawablePadding="6dp"
android:gravity="center_vertical"
android:minHeight="48dp"
android:paddingStart="10dp"
android:paddingEnd="14dp"
android:text="@string/dtc.module.clear"
android:textColor="?android:attr/textColorPrimary"
android:textSize="13sp"
app:drawableStartCompat="@drawable/dtc_clear_module"
app:drawableTint="@color/cardinal" />
</FrameLayout>
8 changes: 8 additions & 0 deletions app/src/main/res/values-pl/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,7 @@
<string name="dtc.action_refresh">Odśwież</string>
<string name="dtc.action_clear_codes">Skasuj kody</string>
<string name="dtc.action_share">Udostępnij</string>
<string name="dtc.action_close">Zamknij</string>
<string name="dtc.action_clearing">Kasowanie…</string>
<string name="dtc.share.chooser_title">Udostępnij raport diagnostyczny</string>
<string name="dtc.share.report_title">Raport diagnostyczny pojazdu</string>
Expand Down Expand Up @@ -787,6 +788,13 @@
<string name="dtc.module.no_response">Brak odpowiedzi</string>
<string name="dtc.module.no_response.explanation">Brak odpowiedzi: moduł nie odpowiedział na odczyt. Jego kody są nieznane, więc nie wpływa na oznaczenia Nowy i Już nie zgłaszane.</string>
<string name="dtc.module.refused">Odmowa</string>
<string name="dtc.module.clear">Skasuj kody tego modułu</string>
<string name="dtc.module.clear.title">Skasować kody modułu %1$s?</string>
<string name="dtc.module.clear.message">Z pojazdu zostaną skasowane tylko kody tego modułu i ich dane zamrożonej klatki; pozostałe moduły zachowają swoje. Aplikacja zachowuje kopię w historii kodów.\n\nUpewnij się, że silnik jest WYŁĄCZONY, ale stacyjka jest WŁĄCZONA.\n\nJeśli to moduł silnika, monitory gotowości również zostaną wyzerowane.</string>
<string name="dtc.module.clear.confirm">Skasuj</string>
<string name="dtc.engine_running.title">Silnik pracuje</string>
<string name="dtc.engine_running.message">Wygląda na to, że silnik pracuje. Większość modułów odmawia skasowania kodów przy pracującym silniku. Wyłącz silnik, zostaw włączoną stacyjkę i spróbuj ponownie.</string>
<string name="dtc.engine_running.clear_anyway">Skasuj mimo to</string>
<string name="dtc.module.refused.explanation">Odmowa: moduł odpowiedział, ale nie przesłał kodów.</string>
<string name="dtc.share.modules_no_response">Moduły bez odpowiedzi: %1$s</string>
<string name="dtc.history.gone_title">Już nie zgłaszane</string>
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,7 @@
<string name="dtc.action_refresh">Refresh</string>
<string name="dtc.action_clear_codes">Clear Codes</string>
<string name="dtc.action_share">Share</string>
<string name="dtc.action_close">Close</string>
<string name="dtc.action_clearing">Clearing…</string>
<string name="dtc.share.chooser_title">Share Diagnostic Report</string>
<string name="dtc.share.report_title">Vehicle Diagnostic Report</string>
Expand Down Expand Up @@ -1012,6 +1013,13 @@
<string name="dtc.module.no_response">No response</string>
<string name="dtc.module.no_response.explanation">No response: the module did not answer the scan. Its codes are unknown, so it is left out of New and No longer reported.</string>
<string name="dtc.module.refused">Refused</string>
<string name="dtc.module.clear">Clear this module</string>
<string name="dtc.module.clear.title">Clear codes in %1$s?</string>
<string name="dtc.module.clear.message">Only this module\'s trouble codes and their freeze frame data are erased from the vehicle; the other modules keep theirs. The app keeps a copy in its code history.\n\nMake sure the engine is OFF, but the ignition is ON.\n\nIf this is the engine module, the readiness monitors reset too.</string>
<string name="dtc.module.clear.confirm">Clear</string>
<string name="dtc.engine_running.title">The engine is running</string>
<string name="dtc.engine_running.message">The engine seems to be running. Most modules refuse to clear codes while it runs. Turn the engine off, leave the ignition on and try again.</string>
<string name="dtc.engine_running.clear_anyway">Clear anyway</string>
<string name="dtc.module.refused.explanation">Refused: the module answered but declined to send its codes.</string>
<string name="dtc.share.modules_no_response">Modules without an answer: %1$s</string>
<string name="dtc.history.gone_title">No longer reported</string>
Expand Down
Loading
Loading