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.