From 79954ad45573e6e418c0c44974f7e0287eb28663 Mon Sep 17 00:00:00 2001
From: migridgal <92881435+Ridawo@users.noreply.github.com>
Date: Sat, 17 Oct 2026 20:26:15 +0200
Subject: [PATCH 01/13] refactor(settings): extract ECU folder path mapping
---
src/InpaDroid/Diag/EcuFolderPath.cs | 26 ++++++++++++++++++++++++++
src/InpaDroid/Ui/SettingsActivity.cs | 22 +++++++++-------------
2 files changed, 35 insertions(+), 13 deletions(-)
create mode 100644 src/InpaDroid/Diag/EcuFolderPath.cs
diff --git a/src/InpaDroid/Diag/EcuFolderPath.cs b/src/InpaDroid/Diag/EcuFolderPath.cs
new file mode 100644
index 0000000..6508f17
--- /dev/null
+++ b/src/InpaDroid/Diag/EcuFolderPath.cs
@@ -0,0 +1,26 @@
+using System.Text.RegularExpressions;
+
+namespace InpaDroid.Diag;
+
+/// Pasa el id de documento del selector de carpetas a la ruta de archivo que necesita EDIABAS.
+public static class EcuFolderPath
+{
+ public const string DefaultPrimaryRoot = "/storage/emulated/0";
+
+ ///
+ /// "primary:Carpeta" -> {primaryRoot}/Carpeta ; "XXXX-XXXX:Carpeta" -> /storage/XXXX-XXXX/Carpeta.
+ /// Null si el volumen no es "primary" ni una tarjeta (Descargas, Documentos, etc. no tienen ruta).
+ ///
+ public static string? FromTreeDocumentId(string? docId, string primaryRoot = DefaultPrimaryRoot)
+ {
+ docId ??= "";
+ int colon = docId.IndexOf(':');
+ string volume = colon >= 0 ? docId[..colon] : docId;
+ string rel = colon >= 0 ? docId[(colon + 1)..] : "";
+ bool primary = volume.Equals("primary", StringComparison.OrdinalIgnoreCase);
+ if (!primary && !Regex.IsMatch(volume, "^[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}$"))
+ return null;
+ string root = primary ? primaryRoot : "/storage/" + volume;
+ return rel.Length > 0 ? Path.Combine(root, rel) : root;
+ }
+}
diff --git a/src/InpaDroid/Ui/SettingsActivity.cs b/src/InpaDroid/Ui/SettingsActivity.cs
index fd43cc4..1284222 100644
--- a/src/InpaDroid/Ui/SettingsActivity.cs
+++ b/src/InpaDroid/Ui/SettingsActivity.cs
@@ -243,25 +243,21 @@ protected override void OnActivityResult(int requestCode, Result resultCode, Int
try
{
// El selector devuelve un URI de documento; EDIABAS necesita una ruta de archivo.
- // "primary:Carpeta" -> /storage/emulated/0/Carpeta ; "XXXX-XXXX:Carpeta" -> /storage/XXXX-XXXX/Carpeta
- var docId = DocumentsContract.GetTreeDocumentId(data.Data) ?? "";
- int colon = docId.IndexOf(':');
- string volume = colon >= 0 ? docId[..colon] : docId;
- string rel = colon >= 0 ? docId[(colon + 1)..] : "";
- // Solo "primary" o un volumen tipo "1A2B-3C4D" se pueden pasar a ruta; Descargas, Documentos, etc. no.
- bool isVolume = volume.Equals("primary", StringComparison.OrdinalIgnoreCase)
- || System.Text.RegularExpressions.Regex.IsMatch(volume, "^[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}$");
- if (data.Data.Authority != "com.android.externalstorage.documents" || !isVolume)
+ if (data.Data.Authority != "com.android.externalstorage.documents")
{
ShowStatus(GetString(Resource.String.st_folder_unusable), true);
return;
}
- string root = volume.Equals("primary", StringComparison.OrdinalIgnoreCase)
#pragma warning disable CS0618, CA1422 // ExternalStorageDirectory obsoleto, pero es la ruta que ve EDIABAS
- ? Android.OS.Environment.ExternalStorageDirectory?.AbsolutePath ?? "/storage/emulated/0"
+ string primaryRoot = Android.OS.Environment.ExternalStorageDirectory?.AbsolutePath ?? EcuFolderPath.DefaultPrimaryRoot;
#pragma warning restore CS0618, CA1422
- : "/storage/" + volume;
- _ecuPath.Text = rel.Length > 0 ? Path.Combine(root, rel) : root;
+ string? path = EcuFolderPath.FromTreeDocumentId(DocumentsContract.GetTreeDocumentId(data.Data), primaryRoot);
+ if (path == null)
+ {
+ ShowStatus(GetString(Resource.String.st_folder_unusable), true);
+ return;
+ }
+ _ecuPath.Text = path;
ShowStatus(GetString(Resource.String.st_folder_chosen), false);
}
catch (Exception ex)
From 7a3c6112d75f73a4087308a40c063b49347665e1 Mon Sep 17 00:00:00 2001
From: migridgal <92881435+Ridawo@users.noreply.github.com>
Date: Sat, 17 Oct 2026 20:26:22 +0200
Subject: [PATCH 02/13] test(settings): cover ECU folder path mapping
---
tests/InpaDroid.Tests/EcuFolderPathTests.cs | 42 ++++++++++++++++++++
tests/InpaDroid.Tests/InpaDroid.Tests.csproj | 1 +
2 files changed, 43 insertions(+)
create mode 100644 tests/InpaDroid.Tests/EcuFolderPathTests.cs
diff --git a/tests/InpaDroid.Tests/EcuFolderPathTests.cs b/tests/InpaDroid.Tests/EcuFolderPathTests.cs
new file mode 100644
index 0000000..dc13cba
--- /dev/null
+++ b/tests/InpaDroid.Tests/EcuFolderPathTests.cs
@@ -0,0 +1,42 @@
+using InpaDroid.Diag;
+
+namespace InpaDroid.Tests;
+
+public class EcuFolderPathTests
+{
+ [Theory]
+ [InlineData("primary:Ecu", "/storage/emulated/0/Ecu")]
+ [InlineData("primary:", "/storage/emulated/0")]
+ [InlineData("primary", "/storage/emulated/0")]
+ [InlineData("PRIMARY:Ecu", "/storage/emulated/0/Ecu")]
+ [InlineData("primary:EDIABAS/Ecu", "/storage/emulated/0/EDIABAS/Ecu")]
+ [InlineData("1A2B-3C4D:Ecu", "/storage/1A2B-3C4D/Ecu")]
+ [InlineData("1a2b-3c4d:EDIABAS/Ecu", "/storage/1a2b-3c4d/EDIABAS/Ecu")]
+ [InlineData("1A2B-3C4D:", "/storage/1A2B-3C4D")]
+ [InlineData("1A2B-3C4D", "/storage/1A2B-3C4D")]
+ [InlineData("primary:Ecu:viejo", "/storage/emulated/0/Ecu:viejo")]
+ public void FromTreeDocumentId_MapsVolumes(string docId, string expected) =>
+ Assert.Equal(expected, EcuFolderPath.FromTreeDocumentId(docId));
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(null)]
+ [InlineData("Ecu")]
+ [InlineData(":Ecu")]
+ [InlineData("downloads:Ecu")]
+ [InlineData("home:Documentos")]
+ [InlineData("raw:/storage/emulated/0/Download/Ecu")]
+ [InlineData("1A2B3C4D:Ecu")]
+ [InlineData("1A2B-3C4G:Ecu")]
+ [InlineData("1A2B-3C4D5:Ecu")]
+ public void FromTreeDocumentId_UnusableVolumeIsNull(string? docId) =>
+ Assert.Null(EcuFolderPath.FromTreeDocumentId(docId));
+
+ [Fact]
+ public void FromTreeDocumentId_UsesGivenPrimaryRoot()
+ {
+ Assert.Equal("/mnt/sdcard/Ecu", EcuFolderPath.FromTreeDocumentId("primary:Ecu", "/mnt/sdcard"));
+ Assert.Equal("/mnt/sdcard", EcuFolderPath.FromTreeDocumentId("primary:", "/mnt/sdcard"));
+ Assert.Equal("/storage/1A2B-3C4D/Ecu", EcuFolderPath.FromTreeDocumentId("1A2B-3C4D:Ecu", "/mnt/sdcard"));
+ }
+}
diff --git a/tests/InpaDroid.Tests/InpaDroid.Tests.csproj b/tests/InpaDroid.Tests/InpaDroid.Tests.csproj
index 7cc784c..1d8da94 100644
--- a/tests/InpaDroid.Tests/InpaDroid.Tests.csproj
+++ b/tests/InpaDroid.Tests/InpaDroid.Tests.csproj
@@ -32,6 +32,7 @@
+
From 8cb7bd20e21a5b7430f553d0fb50c422d475de87 Mon Sep 17 00:00:00 2001
From: migridgal <92881435+Ridawo@users.noreply.github.com>
Date: Sat, 17 Oct 2026 20:26:31 +0200
Subject: [PATCH 03/13] refactor(ecu): move Status polling into
FaultScreenActivity
---
.../Resources/values-de/strings_ch.xml | 1 -
.../Resources/values-en/strings_ch.xml | 1 -
.../Resources/values-pt/strings_ch.xml | 1 -
src/InpaDroid/Resources/values/strings_ch.xml | 1 -
.../Ui/Chassis/ChassisEcuActivity.cs | 28 -----------------
src/InpaDroid/Ui/EcuActivity.cs | 26 ----------------
src/InpaDroid/Ui/FaultScreenActivity.cs | 31 ++++++++++++++++++-
7 files changed, 30 insertions(+), 59 deletions(-)
diff --git a/src/InpaDroid/Resources/values-de/strings_ch.xml b/src/InpaDroid/Resources/values-de/strings_ch.xml
index d82b8fa..3651052 100644
--- a/src/InpaDroid/Resources/values-de/strings_ch.xml
+++ b/src/InpaDroid/Resources/values-de/strings_ch.xml
@@ -41,7 +41,6 @@
Status: %1$s
Lesen…
Stopp
- (gestoppt)
FEHLER kein Ergebnis %1$s
Katalog %1$s ist leer
Steuergerät wählen (%1$s)
diff --git a/src/InpaDroid/Resources/values-en/strings_ch.xml b/src/InpaDroid/Resources/values-en/strings_ch.xml
index 436dc56..f11fe1e 100644
--- a/src/InpaDroid/Resources/values-en/strings_ch.xml
+++ b/src/InpaDroid/Resources/values-en/strings_ch.xml
@@ -41,7 +41,6 @@
Status: %1$s
Reading…
Stop
- (stopped)
ERROR no result %1$s
%1$s catalog is empty
Pick an ECU (%1$s)
diff --git a/src/InpaDroid/Resources/values-pt/strings_ch.xml b/src/InpaDroid/Resources/values-pt/strings_ch.xml
index 2b2e758..ddbad2d 100644
--- a/src/InpaDroid/Resources/values-pt/strings_ch.xml
+++ b/src/InpaDroid/Resources/values-pt/strings_ch.xml
@@ -41,7 +41,6 @@
Status: %1$s
Lendo…
Parar
- (parado)
ERRO sem resultado %1$s
O catálogo %1$s está vazio
Escolha uma ECU (%1$s)
diff --git a/src/InpaDroid/Resources/values/strings_ch.xml b/src/InpaDroid/Resources/values/strings_ch.xml
index fe6a9ca..42c87ed 100644
--- a/src/InpaDroid/Resources/values/strings_ch.xml
+++ b/src/InpaDroid/Resources/values/strings_ch.xml
@@ -41,7 +41,6 @@
Status: %1$s
Leyendo…
Parar
- (detenido)
ERROR sin resultado %1$s
Catálogo %1$s vacío
Elige una centralita (%1$s)
diff --git a/src/InpaDroid/Ui/Chassis/ChassisEcuActivity.cs b/src/InpaDroid/Ui/Chassis/ChassisEcuActivity.cs
index ce0a5a1..abb82eb 100644
--- a/src/InpaDroid/Ui/Chassis/ChassisEcuActivity.cs
+++ b/src/InpaDroid/Ui/Chassis/ChassisEcuActivity.cs
@@ -26,10 +26,7 @@ public class ChassisEcuActivity : FaultScreenActivity
ChassisEcu _ecu = null!;
TextView _status = null!;
bool _pollInFlight;
- readonly PollRunner _poll = new();
- protected override bool PauseHeader => _busyJob || _poll.Running;
- protected override bool KeepBusyBar => _poll.Running; // si la operación ha arrancado el Status, su barra "Parar" se queda
protected override bool InFlight => _busyJob || _pollInFlight;
protected override string EcuSgbd => _ecu.Sgbd;
@@ -88,23 +85,6 @@ protected override void OnCreate(Bundle? savedInstanceState)
void UpdateSubtitle() => SetSubtitle($"{_ecu.Sgbd} → {_sgbd ?? "?"}");
- // ---------------------------------------------------------------- control de teclas
-
- protected override bool BeforeKey(int f, bool shift)
- {
- if (!base.BeforeKey(f, shift))
- return false;
- // Cualquier acción detiene la lectura continua de Status.
- StopPolling();
- return true;
- }
-
- protected override void OnPause()
- {
- StopPolling();
- base.OnPause();
- }
-
string? StatusError(JobResult r) => DiagLogic.StatusError(r, GetString(Resource.String.g_rp_job_failed)!);
// Botón Reintentar del estado vacío: limpia el aviso y vuelve a identificar; si falla, EnsureSgbdAsync lo muestra de nuevo.
@@ -337,12 +317,4 @@ void StartPolling(string sgbd, ChassisPage page)
return true;
}, ex => _status.Text = UiUtil.Describe(ex), StatusPollMs, 100);
}
-
- void StopPolling()
- {
- if (!_poll.Stop())
- return;
- HideBusy();
- _heading.Text += " " + GetString(Resource.String.ch_stopped);
- }
}
diff --git a/src/InpaDroid/Ui/EcuActivity.cs b/src/InpaDroid/Ui/EcuActivity.cs
index 1d689a6..486cc6d 100644
--- a/src/InpaDroid/Ui/EcuActivity.cs
+++ b/src/InpaDroid/Ui/EcuActivity.cs
@@ -19,7 +19,6 @@ public class EcuActivity : FaultScreenActivity
string _ecuName = "";
string _chassis = ""; // chasis de origen (para el historial y el informe); vacío desde la lista de SGBD
bool _isGroup;
- readonly PollRunner _poll = new();
protected override string EcuSgbd => _ecuName;
protected override string VariantBusyText => UiUtil.Str(this, Resource.String.g_variant_resolving, _ecuName);
@@ -32,8 +31,6 @@ public class EcuActivity : FaultScreenActivity
protected override string ClearMessage(string name) => UiUtil.Str(this, Resource.String.g_clear_msg, name);
protected override string ClearYes => GetString(Resource.String.g_clear_yes)!;
- protected override bool PauseHeader => _busyJob || _poll.Running;
-
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
@@ -85,21 +82,6 @@ protected override void OnVariantFound()
UiUtil.Str(this, Resource.String.g_variant_found, _sgbd), append: true);
}
- protected override bool BeforeKey(int f, bool shift)
- {
- if (!base.BeforeKey(f, shift))
- return false;
- // Cualquier acción detiene la lectura continua de Status.
- StopPolling();
- return true;
- }
-
- protected override void OnPause()
- {
- StopPolling();
- base.OnPause();
- }
-
void RunOnce(string job, string heading, string args = "", string results = "") =>
Exclusive(async () =>
{
@@ -187,14 +169,6 @@ void StartPolling(string sgbd, string job, string args, string results)
}, ex => _results.ShowException(sgbd, job, ex), StatusPollMs, 100);
}
- void StopPolling()
- {
- if (!_poll.Stop())
- return;
- HideBusy();
- _heading.Text += " " + GetString(Resource.String.g_stopped);
- }
-
void CopyResults()
{
var text = _results.PlainText();
diff --git a/src/InpaDroid/Ui/FaultScreenActivity.cs b/src/InpaDroid/Ui/FaultScreenActivity.cs
index a317a3a..b1d910a 100644
--- a/src/InpaDroid/Ui/FaultScreenActivity.cs
+++ b/src/InpaDroid/Ui/FaultScreenActivity.cs
@@ -4,7 +4,7 @@ namespace InpaDroid.Ui;
///
/// Pantalla de centralita con memoria de errores (EcuActivity, ChassisEcuActivity): leer y guardar en el historial,
-/// compartir CSV o informe, y borrar con copia de seguridad previa. Cada pantalla pone sus jobs y sus textos.
+/// compartir CSV o informe, borrar con copia de seguridad previa y Status continuo. Cada pantalla pone sus jobs y sus textos.
///
public abstract class FaultScreenActivity : EcuScreenActivity
{
@@ -25,6 +25,35 @@ public abstract class FaultScreenActivity : EcuScreenActivity
/// Se llama con una lectura de errores correcta, antes de guardarla.
protected virtual void OnFaultsRead(JobResult r) { }
+ /// Lectura continua de Status; cada pantalla la arranca a su manera.
+ protected readonly PollRunner _poll = new();
+
+ protected override bool PauseHeader => _busyJob || _poll.Running;
+ protected override bool KeepBusyBar => _poll.Running; // si la operación ha arrancado el Status, su barra "Parar" se queda
+
+ protected override bool BeforeKey(int f, bool shift)
+ {
+ if (!base.BeforeKey(f, shift))
+ return false;
+ // Cualquier acción detiene la lectura continua de Status.
+ StopPolling();
+ return true;
+ }
+
+ protected override void OnPause()
+ {
+ StopPolling();
+ base.OnPause();
+ }
+
+ protected void StopPolling()
+ {
+ if (!_poll.Stop())
+ return;
+ HideBusy();
+ _heading.Text += " " + GetString(Resource.String.g_stopped);
+ }
+
protected void ConfirmClearFaults()
{
var name = _sgbd ?? EcuSgbd;
From 178d1efd81afc3c14098108a0401c507ca2e6b12 Mon Sep 17 00:00:00 2001
From: migridgal <92881435+Ridawo@users.noreply.github.com>
Date: Tue, 20 Oct 2026 17:05:15 +0200
Subject: [PATCH 04/13] refactor(ui): rename Title helper to Label in coding
and history screens
---
src/InpaDroid/Ui/CodingActivity.cs | 8 ++++----
src/InpaDroid/Ui/HistoryActivity.cs | 6 +++---
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/InpaDroid/Ui/CodingActivity.cs b/src/InpaDroid/Ui/CodingActivity.cs
index 786378a..6a07d90 100644
--- a/src/InpaDroid/Ui/CodingActivity.cs
+++ b/src/InpaDroid/Ui/CodingActivity.cs
@@ -148,7 +148,7 @@ void Compare()
Section(sb, Resource.String.cd_cmp_changed, d.Changed.Select(c => $"{c.Key} : {c.Before} → {c.After}").ToList());
Section(sb, Resource.String.cd_cmp_added, d.Added.Select(Line).ToList());
Section(sb, Resource.String.cd_cmp_removed, d.Removed.Select(Line).ToList());
- _heading.Text = $"{Title(cur)} · {cur.Job}";
+ _heading.Text = $"{Label(cur)} · {cur.Job}";
_results.ShowInfo(UiUtil.Str(this, Resource.String.cd_compare_head, FormatWhen(prev)), sb.ToString().TrimEnd());
});
}
@@ -174,7 +174,7 @@ void Snapshots() =>
return;
}
var adapter = new TwoLineAdapter(this, all.Select(s => new TwoLineAdapter.Row(
- Title(s), UiUtil.Str(this, Resource.String.cd_snapshot_sub, FormatWhen(s), JobLabel(s), s.Entries.Count), null, default, s)));
+ Label(s), UiUtil.Str(this, Resource.String.cd_snapshot_sub, FormatWhen(s), JobLabel(s), s.Entries.Count), null, default, s)));
new AlertDialog.Builder(this)
.SetTitle(GetString(Resource.String.cd_key_snapshots))!
.SetAdapter(adapter, (_, e) => ShowSnapshot((CodingBackup.Snapshot)adapter.GetRow(e.Which).Tag))!
@@ -185,7 +185,7 @@ void Snapshots() =>
void ShowSnapshot(CodingBackup.Snapshot s)
{
_current = s;
- _heading.Text = $"{Title(s)} · {s.Job}";
+ _heading.Text = $"{Label(s)} · {s.Job}";
_results.ShowInfo($"{s.Sgbd} : {s.Job} {FormatWhen(s)}",
s.Entries.Count == 0 ? GetString(Resource.String.g_rp_no_results)! : string.Join("\n", s.Entries.Select(Line)));
}
@@ -194,7 +194,7 @@ void ShowSnapshot(CodingBackup.Snapshot s)
static string JobLabel(CodingBackup.Snapshot s) => s.Args.Length > 0 ? $"{s.Job} {s.Args}" : s.Job;
- static string Title(CodingBackup.Snapshot s) => s.EcuTitle.Length > 0 ? s.EcuTitle : s.Sgbd;
+ static string Label(CodingBackup.Snapshot s) => s.EcuTitle.Length > 0 ? s.EcuTitle : s.Sgbd;
static string FormatWhen(CodingBackup.Snapshot s) =>
DateTime.TryParse(s.When, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var t)
diff --git a/src/InpaDroid/Ui/HistoryActivity.cs b/src/InpaDroid/Ui/HistoryActivity.cs
index 12eef56..0c49987 100644
--- a/src/InpaDroid/Ui/HistoryActivity.cs
+++ b/src/InpaDroid/Ui/HistoryActivity.cs
@@ -60,7 +60,7 @@ async void Load()
var readColor = UiUtil.Res(this, Resource.Color.inpa_badge_prg);
var backupColor = UiUtil.Res(this, Resource.Color.inpa_badge_grp);
_list.Adapter = new TwoLineAdapter(this, _sessions.Select(s => new TwoLineAdapter.Row(
- Title(s),
+ Label(s),
UiUtil.Str(this, Resource.String.hs_row_sub, FormatWhen(s), s.Faults.Count),
s.Kind == SessionRecord.KindBackup ? kindBackup : kindRead,
s.Kind == SessionRecord.KindBackup ? backupColor : readColor,
@@ -69,7 +69,7 @@ async void Load()
_empty.Visibility = _sessions.Count == 0 && _selected < 0 ? ViewStates.Visible : ViewStates.Gone;
}
- static string Title(SessionRecord s) => s.EcuTitle.Length > 0 ? s.EcuTitle : s.Sgbd;
+ static string Label(SessionRecord s) => s.EcuTitle.Length > 0 ? s.EcuTitle : s.Sgbd;
static string FormatWhen(SessionRecord s) => s.WhenUtc?.ToLocalTime().ToString("g") ?? s.When;
@@ -92,7 +92,7 @@ void ShowDetail(int index)
_selected = index;
var s = _sessions[index];
var sb = new StringBuilder();
- sb.Append(UiUtil.Str(this, Resource.String.hs_detail_head, Title(s), FormatWhen(s),
+ sb.Append(UiUtil.Str(this, Resource.String.hs_detail_head, Label(s), FormatWhen(s),
s.Kind == SessionRecord.KindBackup ? GetString(Resource.String.hs_kind_backup) : GetString(Resource.String.hs_kind_read)));
sb.Append("\n\n");
if (s.Faults.Count == 0)
From 80369b8ed23e26b35ed567311291ed8063676245 Mon Sep 17 00:00:00 2001
From: migridgal <92881435+Ridawo@users.noreply.github.com>
Date: Tue, 20 Oct 2026 17:05:15 +0200
Subject: [PATCH 05/13] refactor(settings): format string resources through
UiUtil.Str
---
src/InpaDroid/Ui/Settings/BluetoothPanel.cs | 4 ++--
src/InpaDroid/Ui/Settings/UsbPanel.cs | 2 +-
src/InpaDroid/Ui/SettingsActivity.cs | 12 ++++++------
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/InpaDroid/Ui/Settings/BluetoothPanel.cs b/src/InpaDroid/Ui/Settings/BluetoothPanel.cs
index a6c7462..9bf8dd6 100644
--- a/src/InpaDroid/Ui/Settings/BluetoothPanel.cs
+++ b/src/InpaDroid/Ui/Settings/BluetoothPanel.cs
@@ -33,7 +33,7 @@ public void UpdateSelected(string? name)
{
_selected.Text = Address.Length == 0
? _activity.GetString(Resource.String.st_bt_selected_none)
- : _activity.GetString(Resource.String.st_bt_selected, Address) + (string.IsNullOrEmpty(name) ? "" : $" ({name})");
+ : UiUtil.Str(_activity, Resource.String.st_bt_selected, Address) + (string.IsNullOrEmpty(name) ? "" : $" ({name})");
}
bool HasPermission() =>
@@ -99,7 +99,7 @@ public void Refresh()
}
catch (Exception ex)
{
- AddMessage(_activity.GetString(Resource.String.st_bt_error, UiUtil.Describe(ex)), true);
+ AddMessage(UiUtil.Str(_activity, Resource.String.st_bt_error, UiUtil.Describe(ex)), true);
}
}
diff --git a/src/InpaDroid/Ui/Settings/UsbPanel.cs b/src/InpaDroid/Ui/Settings/UsbPanel.cs
index fc5daefb..aab4baa 100644
--- a/src/InpaDroid/Ui/Settings/UsbPanel.cs
+++ b/src/InpaDroid/Ui/Settings/UsbPanel.cs
@@ -55,7 +55,7 @@ public void Update()
}
catch (Exception ex)
{
- SetInfo(_activity.GetString(Resource.String.st_usb_error, UiUtil.Describe(ex)), UiUtil.ErrorText);
+ SetInfo(UiUtil.Str(_activity, Resource.String.st_usb_error, UiUtil.Describe(ex)), UiUtil.ErrorText);
}
}
diff --git a/src/InpaDroid/Ui/SettingsActivity.cs b/src/InpaDroid/Ui/SettingsActivity.cs
index 1284222..cead957 100644
--- a/src/InpaDroid/Ui/SettingsActivity.cs
+++ b/src/InpaDroid/Ui/SettingsActivity.cs
@@ -217,7 +217,7 @@ void PickFolder()
}
catch (Exception ex)
{
- ShowStatus(GetString(Resource.String.st_folder_picker_error, UiUtil.Describe(ex)), true);
+ ShowStatus(UiUtil.Str(this, Resource.String.st_folder_picker_error, UiUtil.Describe(ex)), true);
}
}
@@ -262,27 +262,27 @@ protected override void OnActivityResult(int requestCode, Result resultCode, Int
}
catch (Exception ex)
{
- ShowStatus(GetString(Resource.String.st_folder_invalid, UiUtil.Describe(ex)), true);
+ ShowStatus(UiUtil.Str(this, Resource.String.st_folder_invalid, UiUtil.Describe(ex)), true);
}
}
async void ShowEcuCount(string path)
{
_ecuInfo.SetTextColor(UiUtil.Res(this, Resource.Color.m_text_secondary));
- _ecuInfo.Text = GetString(Resource.String.st_ecu_default_searching, _defaultPath);
+ _ecuInfo.Text = UiUtil.Str(this, Resource.String.st_ecu_default_searching, _defaultPath);
try
{
var svc = Diag;
var files = await Task.Run(() => svc.ScanEcuFolder());
int groups = files.Count(f => f.IsGroup);
- _ecuInfo.Text = GetString(Resource.String.st_ecu_default_count, _defaultPath, path, (files.Count - groups).ToString(), groups.ToString());
+ _ecuInfo.Text = UiUtil.Str(this, Resource.String.st_ecu_default_count, _defaultPath, path, (files.Count - groups).ToString(), groups.ToString());
if (files.Count == 0)
_ecuInfo.SetTextColor(UiUtil.ErrorText);
}
catch (Exception ex)
{
_ecuInfo.SetTextColor(UiUtil.ErrorText);
- _ecuInfo.Text = GetString(Resource.String.st_ecu_default_error, _defaultPath, UiUtil.Describe(ex));
+ _ecuInfo.Text = UiUtil.Str(this, Resource.String.st_ecu_default_error, _defaultPath, UiUtil.Describe(ex));
}
}
@@ -332,7 +332,7 @@ async void Save()
}
catch (Exception ex)
{
- ShowStatus(GetString(Resource.String.st_save_error, UiUtil.Describe(ex)), true);
+ ShowStatus(UiUtil.Str(this, Resource.String.st_save_error, UiUtil.Describe(ex)), true);
}
finally
{
From 378b0f449b4ff60ed85c385ca026ee168075a998 Mon Sep 17 00:00:00 2001
From: migridgal <92881435+Ridawo@users.noreply.github.com>
Date: Tue, 20 Oct 2026 17:05:15 +0200
Subject: [PATCH 06/13] fix(a11y): guard null host in ButtonDelegate and
silence CA1422
---
src/InpaDroid/Ui/InpaActivity.cs | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/InpaDroid/Ui/InpaActivity.cs b/src/InpaDroid/Ui/InpaActivity.cs
index b7a0a64..4e19592 100644
--- a/src/InpaDroid/Ui/InpaActivity.cs
+++ b/src/InpaDroid/Ui/InpaActivity.cs
@@ -16,9 +16,9 @@ internal sealed class ButtonDelegate(Func? isChecked = null) : View.Access
{
public override void OnInitializeAccessibilityNodeInfo(View? host, AccessibilityNodeInfo? info)
{
- base.OnInitializeAccessibilityNodeInfo(host, info);
- if (info == null)
+ if (host == null || info == null)
return;
+ base.OnInitializeAccessibilityNodeInfo(host, info);
if (isChecked == null)
{
info.ClassName = "android.widget.Button";
@@ -26,7 +26,9 @@ public override void OnInitializeAccessibilityNodeInfo(View? host, Accessibility
}
info.ClassName = "android.widget.CheckBox";
info.Checkable = true;
+#pragma warning disable CA1422 // Checked obsoleto en API 36, pero TalkBack lo sigue leyendo en todas
info.Checked = isChecked();
+#pragma warning restore CA1422
}
}
@@ -90,7 +92,9 @@ protected void InitInpa(int layoutId, string title)
{
// Atrás = la tecla "volver" (F10) de la pantalla si existe; si no, el atrás del sistema.
if (_keys[9]?.Action != null) PressKey(10, false);
+#pragma warning disable CA1422 // OnBackPressed obsoleto en API 33, pero sigue cerrando la pantalla en todas
else OnBackPressed();
+#pragma warning restore CA1422
};
SetTitleText(title);
FitSubtitle();
From 7cb2aef03597493e0313ebd412609b0808438e0d Mon Sep 17 00:00:00 2001
From: migridgal <92881435+Ridawo@users.noreply.github.com>
Date: Tue, 20 Oct 2026 17:05:15 +0200
Subject: [PATCH 07/13] refactor(ui): reuse the shared ButtonDelegate in
MainActivity
---
src/InpaDroid/Ui/MainActivity.cs | 12 +-----------
1 file changed, 1 insertion(+), 11 deletions(-)
diff --git a/src/InpaDroid/Ui/MainActivity.cs b/src/InpaDroid/Ui/MainActivity.cs
index 8d44a21..f9f2c65 100644
--- a/src/InpaDroid/Ui/MainActivity.cs
+++ b/src/InpaDroid/Ui/MainActivity.cs
@@ -79,23 +79,13 @@ int MenuColumns()
return (cfg?.ScreenWidthDp ?? 0) >= 600 ? 3 : 2;
}
- // Foco y TalkBack ven la tarjeta como un único botón con título y descripción; el icono es decorativo.
- sealed class ButtonDelegate : View.AccessibilityDelegate
- {
- public override void OnInitializeAccessibilityNodeInfo(View? host, Android.Views.Accessibility.AccessibilityNodeInfo? info)
- {
- base.OnInitializeAccessibilityNodeInfo(host, info);
- if (info != null)
- info.ClassName = "android.widget.Button";
- }
- }
-
LinearLayout MakeCard(string titleText, string descText, int iconRes, bool primary, Action act)
{
var card = new LinearLayout(this) { Orientation = Orientation.Vertical, Clickable = true, Focusable = true };
card.SetBackgroundResource(primary ? Resource.Drawable.bg_card_primary : Resource.Drawable.bg_card);
card.ImportantForAccessibility = ImportantForAccessibility.Yes;
card.ContentDescription = UiUtil.Str(this, Resource.String.ws_card_cd, titleText, descText);
+ // Foco y TalkBack ven la tarjeta como un único botón con título y descripción; el icono es decorativo.
card.SetAccessibilityDelegate(new ButtonDelegate());
int pad = UiUtil.Dp(this, 14);
card.SetPadding(pad, pad, pad, pad);
From 7b7e0fde13777eba3996b88584ce334455136fab Mon Sep 17 00:00:00 2001
From: migridgal <92881435+Ridawo@users.noreply.github.com>
Date: Tue, 20 Oct 2026 17:05:15 +0200
Subject: [PATCH 08/13] fix(ui): clear nullability and obsolete-API warnings
---
src/InpaDroid/Ui/EcuImporter.cs | 2 +-
src/InpaDroid/Ui/ExportProvider.cs | 2 +-
src/InpaDroid/Ui/ResultPanel.cs | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/InpaDroid/Ui/EcuImporter.cs b/src/InpaDroid/Ui/EcuImporter.cs
index 6658679..bd8a885 100644
--- a/src/InpaDroid/Ui/EcuImporter.cs
+++ b/src/InpaDroid/Ui/EcuImporter.cs
@@ -152,7 +152,7 @@ static void Import(ContentResolver resolver, List uris, string
static string DisplayName(ContentResolver resolver, Android.Net.Uri uri)
{
- using var c = resolver.Query(uri, [OpenableColumns.DisplayName], null, null, null);
+ using var c = resolver.Query(uri, [IOpenableColumns.DisplayName], null, null, null);
return c != null && c.MoveToFirst() ? c.GetString(0) ?? "" : "";
}
diff --git a/src/InpaDroid/Ui/ExportProvider.cs b/src/InpaDroid/Ui/ExportProvider.cs
index 66b9318..4934ba5 100644
--- a/src/InpaDroid/Ui/ExportProvider.cs
+++ b/src/InpaDroid/Ui/ExportProvider.cs
@@ -58,7 +58,7 @@ static bool IsValidName(string? name) =>
if (file == null)
return null;
var cursor = new MatrixCursor([IOpenableColumns.DisplayName, IOpenableColumns.Size]);
- cursor.AddRow([file.Name, file.Length()]);
+ cursor.AddRow([new Java.Lang.String(file.Name ?? ""), file.Length()]);
return cursor;
}
diff --git a/src/InpaDroid/Ui/ResultPanel.cs b/src/InpaDroid/Ui/ResultPanel.cs
index 4f31042..27f2c20 100644
--- a/src/InpaDroid/Ui/ResultPanel.cs
+++ b/src/InpaDroid/Ui/ResultPanel.cs
@@ -50,7 +50,7 @@ public string PlainText()
var sb = new StringBuilder();
for (int i = 0; i < _root.ChildCount; i++)
{
- var child = _root.GetChildAt(i);
+ var child = _root.GetChildAt(i)!;
if (child is TextView tv)
sb.AppendLine(WithoutLinks(tv.TextFormatted)).AppendLine();
else if (child.Tag is TableTag tag)
From 5479d16e263724d532fa8c16f8faad3c1cb5d459 Mon Sep 17 00:00:00 2001
From: migridgal <92881435+Ridawo@users.noreply.github.com>
Date: Tue, 20 Oct 2026 17:05:15 +0200
Subject: [PATCH 09/13] docs: replace the local project path in SPEC.md
---
SPEC.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/SPEC.md b/SPEC.md
index 1520e21..5710b3f 100644
--- a/SPEC.md
+++ b/SPEC.md
@@ -5,7 +5,7 @@ https://github.com/uholeschak/ediabaslib) como motor EDIABAS. No incluye ningún
el usuario copia sus `.prg`/`.grp` a una carpeta del móvil.
## Rutas
-- Proyecto: `/home/ridao/Desktop/Pruevas antibraviti/InpaDroid/`
+- Proyecto: la raíz de este repositorio
- App: `InpaDroid/src/InpaDroid/InpaDroid.csproj` (namespace raíz `InpaDroid`)
- ediabaslib: `InpaDroid/external/ediabaslib/` (ya copiado, sin `.git`)
- Datos BMW de referencia (NO van dentro de la APK, están en `.gitignore`):
From f4af9c3da781e65a13ddb471db5135c35fc0861d Mon Sep 17 00:00:00 2001
From: migridgal <92881435+Ridawo@users.noreply.github.com>
Date: Fri, 23 Oct 2026 10:50:36 +0200
Subject: [PATCH 10/13] refactor(settings): move default ECU path and Wi-Fi
host into AdapterSettings, close race in DiagHolder.Reset
---
src/InpaDroid/Diag/AdapterSettings.cs | 5 ++--
src/InpaDroid/Ui/DiagHolder.cs | 40 ++++++++-------------------
src/InpaDroid/Ui/SettingsActivity.cs | 8 ++----
3 files changed, 18 insertions(+), 35 deletions(-)
diff --git a/src/InpaDroid/Diag/AdapterSettings.cs b/src/InpaDroid/Diag/AdapterSettings.cs
index 807ae07..cde8bbe 100644
--- a/src/InpaDroid/Diag/AdapterSettings.cs
+++ b/src/InpaDroid/Diag/AdapterSettings.cs
@@ -7,11 +7,12 @@ public enum AdapterType { UsbKDcan, Enet, Elm327Bluetooth, Elm327Wifi }
public sealed class AdapterSettings
{
private const string PrefsName = "inpadroid";
+ public const string DefaultElmWifiHost = "192.168.0.10:35000";
public AdapterType Type { get; set; } = AdapterType.UsbKDcan;
public string BluetoothAddress { get; set; } = ""; // MAC del ELM327
public string EnetHost { get; set; } = EnetHostRule.Default; // "auto", IP o nombre de host
- public string ElmWifiHost { get; set; } = "192.168.0.10:35000";
+ public string ElmWifiHost { get; set; } = DefaultElmWifiHost;
public string EcuPath { get; set; } = "";
public static AdapterSettings Load(Context ctx)
@@ -54,7 +55,7 @@ public void Save(Context ctx)
}
// /Ecu. Se crea para que el usuario sepa dónde copiar sus .prg/.grp.
- private static string DefaultEcuPath(Context ctx)
+ public static string DefaultEcuPath(Context ctx)
{
string baseDir = ctx.GetExternalFilesDir(null)?.AbsolutePath ?? ctx.FilesDir?.AbsolutePath ?? "";
string ecuPath = Path.Combine(baseDir, "Ecu");
diff --git a/src/InpaDroid/Ui/DiagHolder.cs b/src/InpaDroid/Ui/DiagHolder.cs
index 3223f05..5223130 100644
--- a/src/InpaDroid/Ui/DiagHolder.cs
+++ b/src/InpaDroid/Ui/DiagHolder.cs
@@ -15,14 +15,6 @@ public static class DiagHolder
// porque la interfaz de ediabaslib es estática y el Dispose tardío del viejo cerraría el puerto del nuevo.
static Task _closing = Task.CompletedTask;
- /// Carpeta ECU por defecto: <almacenamiento externo de la app>/Ecu.
- public static string DefaultEcuPath(Context ctx)
- {
- var baseDir = ctx.GetExternalFilesDir(null)?.AbsolutePath ?? ctx.FilesDir?.AbsolutePath ?? "";
- return Path.Combine(baseDir, "Ecu");
- }
-
- /// Ajustes guardados, con la carpeta ECU por defecto si no hay ninguna.
/// Carga los catálogos de chasis en el idioma de los recursos de la app, con los override de la carpeta ECU.
public static void LoadCatalogs(Context ctx, string ecuPath)
{
@@ -30,14 +22,8 @@ public static void LoadCatalogs(Context ctx, string ecuPath)
ChassisCatalog.TryLoadFromFolder(ecuPath);
}
- public static AdapterSettings LoadSettings(Context ctx)
- {
- var app = ctx.ApplicationContext ?? ctx;
- var settings = AdapterSettings.Load(app);
- if (string.IsNullOrWhiteSpace(settings.EcuPath))
- settings.EcuPath = DefaultEcuPath(app);
- return settings;
- }
+ /// Ajustes guardados, con la carpeta ECU por defecto si no hay ninguna.
+ public static AdapterSettings LoadSettings(Context ctx) => AdapterSettings.Load(ctx.ApplicationContext ?? ctx);
/// Devuelve el servicio de la app (lo crea si hace falta). Puede lanzar excepción.
public static DiagService Get(Context ctx)
@@ -60,22 +46,20 @@ public static DiagService Get(Context ctx)
/// Libera el servicio actual (en segundo plano); el siguiente Get crea uno nuevo.
public static Task Reset()
{
- DiagService? old;
+ // _closing se publica en el mismo lock que vacía _service: un Get entre medias abriría el cable sin esperar al cierre.
lock (Sync)
{
- old = _service;
+ var old = _service;
_service = null;
+ // Sin servicio puede seguir en curso un cierre anterior: quien espera a Reset espera también a ese.
+ if (old == null)
+ return _closing;
+ return _closing = Task.Run(() =>
+ {
+ try { old.Abort(); } catch { }
+ try { old.Dispose(); } catch { }
+ });
}
- if (old == null)
- return Task.CompletedTask;
- var closing = Task.Run(() =>
- {
- try { old.Abort(); } catch { }
- try { old.Dispose(); } catch { }
- });
- lock (Sync)
- _closing = closing;
- return closing;
}
public static void TryCreateDirectory(string path)
diff --git a/src/InpaDroid/Ui/SettingsActivity.cs b/src/InpaDroid/Ui/SettingsActivity.cs
index cead957..a07922f 100644
--- a/src/InpaDroid/Ui/SettingsActivity.cs
+++ b/src/InpaDroid/Ui/SettingsActivity.cs
@@ -51,7 +51,7 @@ protected override void OnCreate(Bundle? savedInstanceState)
_wifiError = FindViewById(Resource.Id.set_wifi_error)!;
AddAdapterHelpViews();
- _defaultPath = DiagHolder.DefaultEcuPath(this);
+ _defaultPath = AdapterSettings.DefaultEcuPath(this);
var s = DiagHolder.LoadSettings(this);
_adapterGroup.Check(s.Type switch
{
@@ -180,9 +180,7 @@ void AddAdapterHelpViews()
{
var parent = (ViewGroup)_adapterGroup.Parent!;
int index = parent.IndexOfChild(_adapterGroup) + 1;
- int p = UiUtil.Dp(this, 4);
-
- p = UiUtil.Dp(this, 12);
+ int p = UiUtil.Dp(this, 12);
_klineNote = new TextView(this) { Text = GetString(Resource.String.st_kline_note) };
var note = _klineNote;
note.SetPadding(p, p, p, p);
@@ -312,7 +310,7 @@ async void Save()
string wifiRaw = _wifiHost.Text?.Trim() ?? "";
s.EnetHost = EnetHostRule.Normalize(_enetHost.Text);
- s.ElmWifiHost = wifiRaw.Length == 0 ? "192.168.0.10:35000" : wifiRaw;
+ s.ElmWifiHost = wifiRaw.Length == 0 ? AdapterSettings.DefaultElmWifiHost : wifiRaw;
s.EcuPath = string.IsNullOrWhiteSpace(_ecuPath.Text) ? _defaultPath : _ecuPath.Text.Trim();
_ecuPath.Text = s.EcuPath;
DiagHolder.TryCreateDirectory(s.EcuPath);
From 05aee63a716cca5526ccd3efcef30a66bd703384 Mon Sep 17 00:00:00 2001
From: migridgal <92881435+Ridawo@users.noreply.github.com>
Date: Fri, 23 Oct 2026 10:50:36 +0200
Subject: [PATCH 11/13] fix(scan): read the ECU folder off the UI thread
---
src/InpaDroid/Ui/Chassis/ChassisScanActivity.cs | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/InpaDroid/Ui/Chassis/ChassisScanActivity.cs b/src/InpaDroid/Ui/Chassis/ChassisScanActivity.cs
index 628597e..1fc3240 100644
--- a/src/InpaDroid/Ui/Chassis/ChassisScanActivity.cs
+++ b/src/InpaDroid/Ui/Chassis/ChassisScanActivity.cs
@@ -173,13 +173,14 @@ async Task ScanEcuAsync(ChassisEcu ecu)
{
try
{
- var variant = await Diag.ResolveVariantAsync(ecu.Sgbd);
+ var svc = Diag;
+ var variant = await svc.ResolveVariantAsync(ecu.Sgbd);
if (string.IsNullOrWhiteSpace(variant))
{
- var files = Diag.ScanEcuFolder().Select(f => f.Name);
+ var files = (await Task.Run(svc.ScanEcuFolder)).Select(f => f.Name);
return new ScanEntry(ecu.Title, ecu.Sgbd, "", ScanSummary.NoVariantState(ecu.Sgbd, files));
}
- var r = await Diag.RunJobAsync(variant, ecu.FsReadJob);
+ var r = await svc.RunJobAsync(variant, ecu.FsReadJob);
var (state, faults, detail) = ScanSummary.Classify(r, GetString(Resource.String.g_rp_job_failed)!);
return new ScanEntry(ecu.Title, ecu.Sgbd, variant, state, faults, detail);
}
From 84a62ed161d9527492042c24ce07da3fd49823f7 Mon Sep 17 00:00:00 2001
From: migridgal <92881435+Ridawo@users.noreply.github.com>
Date: Fri, 23 Oct 2026 10:50:36 +0200
Subject: [PATCH 12/13] fix(diag): report missing USB permission and adapter
errors instead of "no reply"
---
src/InpaDroid/Diag/DiagLogic.cs | 4 ++
src/InpaDroid/Diag/DiagService.cs | 45 ++++++++++++++++-----
src/InpaDroid/Ui/ConnectionCheckActivity.cs | 22 ++++++++--
src/InpaDroid/Ui/InpaActivity.cs | 2 +-
tests/InpaDroid.Tests/ErrorMessageTests.cs | 2 +
5 files changed, 62 insertions(+), 13 deletions(-)
diff --git a/src/InpaDroid/Diag/DiagLogic.cs b/src/InpaDroid/Diag/DiagLogic.cs
index ae52625..a682231 100644
--- a/src/InpaDroid/Diag/DiagLogic.cs
+++ b/src/InpaDroid/Diag/DiagLogic.cs
@@ -29,6 +29,8 @@ internal static class DiagLogic
public const string CancelledText = "Cancelled";
public const string ServiceClosedText = "Diagnostic service closed";
public static string TimeoutText(int seconds) => $"Timeout: no response after {seconds} s";
+ // Prefijo que DiagService antepone al error de EDIABAS si el cable USB está conectado pero sin permiso.
+ public const string UsbPermissionText = "USB permission not granted";
// Solo códigos EDIABAS cuyo significado consta en la tabla de EdiabasNet (ErrorDescription).
private static readonly Dictionary CodeKinds = new(StringComparer.Ordinal)
@@ -74,6 +76,8 @@ public static ErrorKind Classify(string? raw)
return ErrorKind.ServiceClosed;
if (raw.StartsWith("Timeout:", StringComparison.Ordinal))
return ErrorKind.Timeout;
+ if (raw.StartsWith(UsbPermissionText, StringComparison.Ordinal))
+ return ErrorKind.UsbPermission;
foreach (Match m in CodeRegex.Matches(raw))
{
if (CodeKinds.TryGetValue(m.Groups[1].Value.ToUpperInvariant() + "-" + m.Groups[2].Value, out var kind))
diff --git a/src/InpaDroid/Diag/DiagService.cs b/src/InpaDroid/Diag/DiagService.cs
index bb0fdd2..40abed1 100644
--- a/src/InpaDroid/Diag/DiagService.cs
+++ b/src/InpaDroid/Diag/DiagService.cs
@@ -115,20 +115,23 @@ public Task ResolveVariantAsync(string sgbd) =>
return Path.GetFileNameWithoutExtension(_ediabas.SgbdFileName ?? "").ToUpperInvariant();
}, _ => "");
- public Task<(double? Ubatt, bool? Ignition)> ReadBatteryIgnitionAsync() =>
- Enqueue<(double?, bool?)>(() =>
+ // Error: texto técnico de la primera lectura que lanzó excepción (null si ninguna), para distinguir un
+ // adaptador que no abre de un coche que no contesta.
+ public Task<(double? Ubatt, bool? Ignition, string? Error)> ReadBatteryIgnitionAsync() =>
+ Enqueue<(double?, bool?, string?)>(() =>
{
+ string? error = null;
double? ubatt = null;
- double? mv = FindNumber(TryRunUtilityJob("STATUS_UBATT"), "STAT_UBATT_WERT", "STAT_UBATT");
+ double? mv = FindNumber(TryRunUtilityJob("STATUS_UBATT", ref error), "STAT_UBATT_WERT", "STAT_UBATT");
if (mv != null)
{
// UTILITY devuelve mV; si el valor ya parece estar en voltios se deja tal cual.
ubatt = mv > 100 ? mv / 1000.0 : mv;
}
- double? ignition = FindNumber(TryRunUtilityJob("STATUS_ZUENDUNG"), "STAT_ZUENDUNG");
- return (ubatt, ignition == null ? (bool?)null : ignition != 0);
- }, _ => (null, null));
+ double? ignition = FindNumber(TryRunUtilityJob("STATUS_ZUENDUNG", ref error), "STAT_ZUENDUNG");
+ return (ubatt, ignition == null ? (bool?)null : ignition != 0, error);
+ }, error => (null, null, error));
// Cancela el job en curso y los encolados hasta ahora; los que se encolen después no se ven afectados.
// No toca _abort: el hilo de trabajo lo pone a false al empezar cada job y, si coincidiera con este Abort,
@@ -272,7 +275,30 @@ private static string TimeoutMessage(TimeSpan limit) =>
DiagLogic.TimeoutText((int)limit.TotalSeconds);
// Texto técnico de EDIABAS; la UI lo traduce con DiagLogic.Classify.
- private static string DescribeError(Exception ex) => EdiabasNet.GetExceptionText(ex, false, false);
+ // Sin permiso USB, EDIABAS solo dice que no pudo inicializar la interfaz (IFH-0018); se antepone la causa real.
+ private string DescribeError(Exception ex)
+ {
+ string text = EdiabasNet.GetExceptionText(ex, false, false);
+ return DiagLogic.Classify(text) == ErrorKind.InterfaceNotFound && UsbPermissionMissing()
+ ? DiagLogic.UsbPermissionText + ": " + text
+ : text;
+ }
+
+ private bool UsbPermissionMissing()
+ {
+ if (_settings.Type != AdapterType.UsbKDcan || _context.GetSystemService(Context.UsbService) is not UsbManager usb)
+ {
+ return false;
+ }
+ try
+ {
+ return EdFtdiInterface.GetDriverList(usb).Any(d => !usb.HasPermission(d.Device));
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
// Cancelado por el watchdog o por un Abort posterior a encolar el job en curso. Se lee en el hilo de trabajo.
private bool IsAborted => _abort || _jobGeneration != Volatile.Read(ref _abortGeneration);
@@ -438,7 +464,7 @@ private bool IsGroupFile(string sgbd)
return ScanEcuFolder().Any(f => f.IsGroup && f.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
- private List>? TryRunUtilityJob(string job)
+ private List>? TryRunUtilityJob(string job, ref string? error)
{
try
{
@@ -456,8 +482,9 @@ private bool IsGroupFile(string sgbd)
}
return sets;
}
- catch (Exception)
+ catch (Exception ex)
{
+ error ??= IsAborted ? DiagLogic.CancelledText : DescribeError(ex);
return null;
}
}
diff --git a/src/InpaDroid/Ui/ConnectionCheckActivity.cs b/src/InpaDroid/Ui/ConnectionCheckActivity.cs
index 9a7d999..c20a9fe 100644
--- a/src/InpaDroid/Ui/ConnectionCheckActivity.cs
+++ b/src/InpaDroid/Ui/ConnectionCheckActivity.cs
@@ -208,17 +208,23 @@ async void RunTest()
bool utility = UpdateFiles();
double? ubatt = null;
bool? ignition = null;
+ string? error = null;
+ bool aborted = false;
if (adapterReady && utility)
{
_testing = true;
_testBtn.Enabled = false;
_importBtn.Enabled = false;
_testResult.Text = "";
- ShowBusy(GetString(Resource.String.ws_test_busy)!, GetString(Resource.String.g_abort), AbortJob);
+ ShowBusy(GetString(Resource.String.ws_test_busy)!, GetString(Resource.String.g_abort), () =>
+ {
+ aborted = true;
+ AbortJob();
+ });
try
{
// Misma lectura de solo lectura que la cabecera, por la cola de DiagService.
- (ubatt, ignition) = await Diag.ReadBatteryIgnitionAsync();
+ (ubatt, ignition, error) = await Diag.ReadBatteryIgnitionAsync();
}
catch (Exception)
{
@@ -232,6 +238,16 @@ async void RunTest()
HideBusy();
}
}
+ // Un job abortado no dice nada del coche: diagnosticarlo daría "no contesta".
+ if (aborted)
+ {
+ _testResult.SetTextColor(UiUtil.WarnText);
+ _testResult.Text = GetString(Resource.String.g_cancelled);
+ return;
+ }
+ // Si el adaptador ni se abrió (sin permiso USB, puerto ocupado), el fallo es suyo y no del coche.
+ if (ubatt == null && DiagLogic.Classify(error) is ErrorKind.InterfaceNotFound or ErrorKind.UsbPermission or ErrorKind.PortOpen)
+ adapterReady = false;
string volts = ubatt?.ToString("0.0", CultureInfo.InvariantCulture) ?? "";
bool enet = DiagHolder.LoadSettings(this).Type == AdapterType.Enet;
// Solo importa saber si el móvil está en una red local (WiFi/Ethernet); no se pudo comprobar -> null.
@@ -240,7 +256,7 @@ async void RunTest()
_testResult.SetTextColor(fault == ConnectionFault.None ? UiUtil.OkText : UiUtil.ErrorText);
_testResult.Text = fault switch
{
- ConnectionFault.Adapter => GetString(Resource.String.ws_test_adapter),
+ ConnectionFault.Adapter => GetString(Resource.String.ws_test_adapter) + (error == null ? "" : "\n" + UiUtil.Friendly(this, error)),
ConnectionFault.EcuFiles => GetString(Resource.String.ws_test_files),
ConnectionFault.NoReply => GetString(Resource.String.ws_test_noreply),
ConnectionFault.EnetNoNetwork => GetString(Resource.String.en_test_nonet),
diff --git a/src/InpaDroid/Ui/InpaActivity.cs b/src/InpaDroid/Ui/InpaActivity.cs
index 4e19592..955d3d5 100644
--- a/src/InpaDroid/Ui/InpaActivity.cs
+++ b/src/InpaDroid/Ui/InpaActivity.cs
@@ -361,7 +361,7 @@ async void StartHeaderPolling()
}
try
{
- (ubatt, ignition) = await Diag.ReadBatteryIgnitionAsync();
+ (ubatt, ignition, _) = await Diag.ReadBatteryIgnitionAsync();
}
catch
{
diff --git a/tests/InpaDroid.Tests/ErrorMessageTests.cs b/tests/InpaDroid.Tests/ErrorMessageTests.cs
index 2999cff..2c76b83 100644
--- a/tests/InpaDroid.Tests/ErrorMessageTests.cs
+++ b/tests/InpaDroid.Tests/ErrorMessageTests.cs
@@ -37,5 +37,7 @@ public void Classify_ServiceTexts()
Assert.Equal(ErrorKind.Timeout, DiagLogic.Classify(DiagLogic.TimeoutText(60)));
Assert.Equal(ErrorKind.Cancelled, DiagLogic.Classify(DiagLogic.CancelledText));
Assert.Equal(ErrorKind.ServiceClosed, DiagLogic.Classify(DiagLogic.ServiceClosedText));
+ // The prefix wins over the IFH-0018 code that follows it.
+ Assert.Equal(ErrorKind.UsbPermission, DiagLogic.Classify(DiagLogic.UsbPermissionText + ": Error occurred: EDIABAS_IFH_0018"));
}
}
From 7fa25b1b802bffd053afe8c6409c2e2b4ecc0594 Mon Sep 17 00:00:00 2001
From: migridgal <92881435+Ridawo@users.noreply.github.com>
Date: Fri, 23 Oct 2026 10:50:36 +0200
Subject: [PATCH 13/13] docs(notices): add usb-serial-for-android
---
THIRD_PARTY_NOTICES.md | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index 2fff9ff..d460f84 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -6,6 +6,34 @@ InpaDroid is distributed under the GNU GPL v3 (see [LICENSE](LICENSE)). It inclu
EdiabasLib, from the ediabaslib project, vendored under `external/ediabaslib/` (project references to `EdiabasLib` and `UsbSerialNetBinding`). Licensed under the GNU GPL v3. Its license and notices are kept in that directory.
+## usb-serial-for-android
+
+USB serial driver for FTDI (K+DCAN) cables, from [mik3y/usb-serial-for-android](https://github.com/mik3y/usb-serial-for-android). It ships in the APK as the prebuilt `usbSerialForAndroid-release.aar` wrapped by `external/ediabaslib/EdiabasLib/AndroidNetBindings/UsbSerialNetBinding`. Licensed under the MIT License (the copyright line must match the upstream LICENSE.txt of the version bundled in the .aar):
+
+```text
+MIT License
+
+Copyright (c) the usb-serial-for-android authors (see LICENSE.txt in the upstream repository)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
## NuGet packages
Versions as declared in `src/InpaDroid/InpaDroid.csproj`. Check each package page on nuget.org for its license text.