From f52fe6563dc5f45273baa73efa07a84bf979ca16 Mon Sep 17 00:00:00 2001 From: Tomek Zebrowski Date: Mon, 23 Mar 2026 10:48:09 +0100 Subject: [PATCH 1/4] feat: unify screen loading state indicator --- ...sticTroubleCodePreferenceDialogFragment.kt | 47 ++++++++++++++----- app/src/main/res/layout/dialog_dtc.xml | 10 ---- app/src/main/res/values/strings.xml | 1 + 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/obd/graphs/preferences/dtc/DiagnosticTroubleCodePreferenceDialogFragment.kt b/app/src/main/java/org/obd/graphs/preferences/dtc/DiagnosticTroubleCodePreferenceDialogFragment.kt index 91211c26..d66a0f4c 100644 --- a/app/src/main/java/org/obd/graphs/preferences/dtc/DiagnosticTroubleCodePreferenceDialogFragment.kt +++ b/app/src/main/java/org/obd/graphs/preferences/dtc/DiagnosticTroubleCodePreferenceDialogFragment.kt @@ -18,19 +18,26 @@ package org.obd.graphs.preferences.dtc import android.content.Intent import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.ProgressBar +import android.widget.Toast import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import org.obd.graphs.R +import org.obd.graphs.SCREEN_LOCK_PROGRESS_EVENT +import org.obd.graphs.SCREEN_UNLOCK_PROGRESS_EVENT import org.obd.graphs.bl.datalogger.DATA_LOGGER_DTC_ACTION_COMPLETED import org.obd.graphs.bl.datalogger.DataLoggerRepository import org.obd.graphs.bl.datalogger.VehicleCapabilitiesManager +import org.obd.graphs.bl.datalogger.dataLoggerSettings import org.obd.graphs.preferences.CoreDialogFragment import org.obd.graphs.registerReceiver +import org.obd.graphs.sendBroadcastEvent import org.obd.graphs.ui.common.toast import org.obd.graphs.ui.withDataLogger import org.obd.metrics.api.model.DiagnosticTroubleCode @@ -44,6 +51,15 @@ internal class DiagnosticTroubleCodePreferenceDialogFragment : CoreDialogFragmen private lateinit var progressBar: ProgressBar private lateinit var recyclerView: RecyclerView + private val handler = Handler(Looper.getMainLooper()) + private val timeoutRunnable = + Runnable { + if (isAdded) { + setLoadingState(false) + Toast.makeText(requireContext(), "Timeout waiting for vehicle response", Toast.LENGTH_SHORT).show() + } + } + private val dtcNotificationsReceiver = object : android.content.BroadcastReceiver() { override fun onReceive( @@ -66,7 +82,6 @@ internal class DiagnosticTroubleCodePreferenceDialogFragment : CoreDialogFragmen val root = inflater.inflate(R.layout.dialog_dtc, container, false) val sortedDtcList = diagnosticTroubleCodes() - progressBar = root.findViewById(R.id.progress_bar) recyclerView = root.findViewById(R.id.recycler_view) adapter = DiagnosticTroubleCodeViewAdapter(context) @@ -103,6 +118,7 @@ internal class DiagnosticTroubleCodePreferenceDialogFragment : CoreDialogFragmen refreshButton.setOnClickListener { if (DataLoggerRepository.isRunning()) { setLoadingState(true) + startTimeoutTimer() withDataLogger { scheduleDTCRead() } @@ -120,6 +136,7 @@ internal class DiagnosticTroubleCodePreferenceDialogFragment : CoreDialogFragmen ).setPositiveButton("Clear Codes") { dialog, _ -> if (DataLoggerRepository.isRunning()) { setLoadingState(true) + startTimeoutTimer() withDataLogger { scheduleDTCCleanup() } @@ -135,18 +152,21 @@ internal class DiagnosticTroubleCodePreferenceDialogFragment : CoreDialogFragmen } } - private fun setLoadingState(isLoading: Boolean) { + private fun setLoadingState(isLoading: Boolean) = if (isLoading) { - progressBar.visibility = View.VISIBLE - recyclerView.alpha = 0.5f - refreshButton.isEnabled = false - clearButton.isEnabled = false + sendBroadcastEvent( + SCREEN_LOCK_PROGRESS_EVENT, + context?.getText(R.string.pref_dtc_screen_lock) as String, + ) } else { - progressBar.visibility = View.GONE - recyclerView.alpha = 1.0f - refreshButton.isEnabled = true - clearButton.isEnabled = true + sendBroadcastEvent( + SCREEN_UNLOCK_PROGRESS_EVENT, + ) } + + private fun startTimeoutTimer() { + handler.removeCallbacks(timeoutRunnable) + handler.postDelayed(timeoutRunnable, 5000) } private fun shareDtcReport(dtcList: List) { @@ -178,7 +198,7 @@ internal class DiagnosticTroubleCodePreferenceDialogFragment : CoreDialogFragmen reportBuilder.append("Status: $activeStatuses (Hex: $hex)\n") val snapshot = code.snapshot - if (snapshot != null) { + if (snapshot != null && dataLoggerSettings.instance().adapter.dtcReadSnapshots) { reportBuilder.append("Snapshot (Record ${snapshot.size}):\n") snapshot.forEach { did -> val value = did.decodedValue ?: "N/A" @@ -188,7 +208,7 @@ internal class DiagnosticTroubleCodePreferenceDialogFragment : CoreDialogFragmen } } - reportBuilder.append("\n") // Blank line between codes + reportBuilder.append("\n") } val sendIntent: Intent = @@ -247,9 +267,12 @@ internal class DiagnosticTroubleCodePreferenceDialogFragment : CoreDialogFragmen override fun onPause() { super.onPause() requireContext().unregisterReceiver(dtcNotificationsReceiver) + handler.removeCallbacks(timeoutRunnable) } private fun handleDTCChangedNotification() { + handler.removeCallbacks(timeoutRunnable) + setLoadingState(false) val newCodes = diagnosticTroubleCodes() diff --git a/app/src/main/res/layout/dialog_dtc.xml b/app/src/main/res/layout/dialog_dtc.xml index 72cac638..182e39bf 100644 --- a/app/src/main/res/layout/dialog_dtc.xml +++ b/app/src/main/res/layout/dialog_dtc.xml @@ -54,16 +54,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/header_divider" /> - - Are you sure you want to clear all DTCs from the ECU?\n\nMake sure the engine is OFF, but the ignition is ON. Clear command sent to ECU Clear Diagnostic Codes? + This might take a while. Please wait… There is no ECU connection established. Please connect first. From 29046fe853b17f40a8a2e28568d72e06ff1b13ce Mon Sep 17 00:00:00 2001 From: Tomek Zebrowski Date: Mon, 23 Mar 2026 12:52:47 +0100 Subject: [PATCH 2/4] feat: introduce ScreenLockManager component --- .../org/obd/graphs/activity/Components.kt | 21 ------ .../org/obd/graphs/activity/MainActivity.kt | 18 ++--- .../java/org/obd/graphs/activity/Receivers.kt | 13 ++-- .../obd/graphs/activity/ScreenLockManager.kt | 65 +++++++++++++++++++ ...sticTroubleCodePreferenceDialogFragment.kt | 7 +- 5 files changed, 81 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/org/obd/graphs/activity/ScreenLockManager.kt diff --git a/app/src/main/java/org/obd/graphs/activity/Components.kt b/app/src/main/java/org/obd/graphs/activity/Components.kt index 0d949768..d3ba116e 100644 --- a/app/src/main/java/org/obd/graphs/activity/Components.kt +++ b/app/src/main/java/org/obd/graphs/activity/Components.kt @@ -16,11 +16,8 @@ */ package org.obd.graphs.activity -import android.view.View import android.widget.Chronometer import android.widget.ProgressBar -import android.widget.TextView -import androidx.appcompat.app.AlertDialog import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import com.google.android.material.bottomappbar.BottomAppBar @@ -46,21 +43,3 @@ fun MainActivity.leftAppBar(func: (p: NavigationView) -> Unit) { fun MainActivity.navController(func: (p: NavController) -> Unit) { func((supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController) } - -fun MainActivity.lockScreenDialogShow(func: (dialogTitle: TextView) -> Unit) { - lockScreenDialog?.let { - if (it.isShowing) { - it.dismiss() - } - } - - AlertDialog.Builder(this).run { - setCancelable(false) - val dialogView: View = layoutInflater.inflate(R.layout.dialog_screen_lock, null) - val dialogTitle = dialogView.findViewById(R.id.dialog_screen_lock_message_id) - func(dialogTitle) - setView(dialogView) - lockScreenDialog = create() - lockScreenDialog.show() - } -} diff --git a/app/src/main/java/org/obd/graphs/activity/MainActivity.kt b/app/src/main/java/org/obd/graphs/activity/MainActivity.kt index 1a84ce16..7278aaa7 100644 --- a/app/src/main/java/org/obd/graphs/activity/MainActivity.kt +++ b/app/src/main/java/org/obd/graphs/activity/MainActivity.kt @@ -25,7 +25,6 @@ import android.os.StrictMode import android.os.StrictMode.ThreadPolicy import android.os.StrictMode.VmPolicy import android.view.View -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout @@ -59,7 +58,9 @@ const val LOG_TAG = "MainActivity" class MainActivity : AppCompatActivity(), EasyPermissions.PermissionCallbacks { - lateinit var lockScreenDialog: AlertDialog + + internal val screenLockManager = ScreenLockManager(this) + internal lateinit var backupManager: BackupManager internal var activityBroadcastReceiver = @@ -157,7 +158,7 @@ class MainActivity : it.visibility = View.GONE } - setupLockScreenDialog() + screenLockManager.setup() supportActionBar?.hide() setupMetricsProcessors() backupManager = BackupManager(this) @@ -189,6 +190,7 @@ class MainActivity : sendBroadcastEvent(MAIN_ACTIVITY_EVENT_DESTROYED) } + private fun setupExceptionHandler() { Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler()) } @@ -207,16 +209,6 @@ class MainActivity : cacheManager.initCache(cache) } - private fun setupLockScreenDialog() { - AlertDialog.Builder(this).run { - setCancelable(false) - - val dialogView: View = this@MainActivity.layoutInflater.inflate(R.layout.dialog_screen_lock, null) - setView(dialogView) - lockScreenDialog = create() - } - } - private fun setupStrictMode() { if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy( diff --git a/app/src/main/java/org/obd/graphs/activity/Receivers.kt b/app/src/main/java/org/obd/graphs/activity/Receivers.kt index bbcda612..f979724d 100644 --- a/app/src/main/java/org/obd/graphs/activity/Receivers.kt +++ b/app/src/main/java/org/obd/graphs/activity/Receivers.kt @@ -144,14 +144,13 @@ internal fun MainActivity.receive(intent: Intent?) { toast(org.obd.graphs.commons.R.string.main_activity_toast_connection_wifi_incorrect_ssid) } + SCREEN_UNLOCK_PROGRESS_EVENT -> screenLockManager.dismiss() SCREEN_LOCK_PROGRESS_EVENT -> { - lockScreenDialogShow { dialogTitle -> - var msg = intent.getExtraParam() - if (msg.isEmpty()) { - msg = getText(R.string.dialog_screen_lock_message) as String - } - dialogTitle.text = msg + var msg = intent.getExtraParam() + if (msg.isEmpty()) { + msg = getText(R.string.dialog_screen_lock_message) as String } + screenLockManager.show(msg) } AA_EDIT_PREF_SCREEN -> navigateToPreferencesScreen("pref.aa") @@ -169,7 +168,7 @@ internal fun MainActivity.receive(intent: Intent?) { toast(R.string.pref_usb_device_attached, usbDevice.productName!!) } - SCREEN_UNLOCK_PROGRESS_EVENT -> lockScreenDialog.dismiss() + DATA_LOGGER_DTC_AVAILABLE -> if (Prefs.isEnabled("pref.dtc.show_notification")) { diff --git a/app/src/main/java/org/obd/graphs/activity/ScreenLockManager.kt b/app/src/main/java/org/obd/graphs/activity/ScreenLockManager.kt new file mode 100644 index 00000000..1bffb464 --- /dev/null +++ b/app/src/main/java/org/obd/graphs/activity/ScreenLockManager.kt @@ -0,0 +1,65 @@ + /** + * Copyright 2019-2026, Tomasz Żebrowski + * + *

Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.obd.graphs.activity + +import android.app.Activity +import android.view.View +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import org.obd.graphs.R + +class ScreenLockManager( + private val activity: Activity, +) : DefaultLifecycleObserver { + private var lockScreenDialog: AlertDialog? = null + + fun setup() { + AlertDialog.Builder(activity).run { + setCancelable(false) + val dialogView: View = activity.layoutInflater.inflate(R.layout.dialog_screen_lock, null) + setView(dialogView) + lockScreenDialog = create() + } + } + + fun show(message: String) { + lockScreenDialog?.let { dialog -> + val dialogTitle = dialog.findViewById(R.id.dialog_screen_lock_message_id) + if (dialogTitle != null && message.isNotEmpty()) { + dialogTitle.text = message + } + if (!dialog.isShowing) { + dialog.show() + } + } + } + + fun dismiss() { + if (lockScreenDialog?.isShowing == true) { + lockScreenDialog?.dismiss() + } + } + + // Automatically called when MainActivity is destroyed + override fun onDestroy(owner: LifecycleOwner) { + dismiss() + lockScreenDialog = null + super.onDestroy(owner) + } +} diff --git a/app/src/main/java/org/obd/graphs/preferences/dtc/DiagnosticTroubleCodePreferenceDialogFragment.kt b/app/src/main/java/org/obd/graphs/preferences/dtc/DiagnosticTroubleCodePreferenceDialogFragment.kt index d66a0f4c..27ce01ea 100644 --- a/app/src/main/java/org/obd/graphs/preferences/dtc/DiagnosticTroubleCodePreferenceDialogFragment.kt +++ b/app/src/main/java/org/obd/graphs/preferences/dtc/DiagnosticTroubleCodePreferenceDialogFragment.kt @@ -24,7 +24,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button -import android.widget.ProgressBar import android.widget.Toast import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -48,12 +47,14 @@ internal class DiagnosticTroubleCodePreferenceDialogFragment : CoreDialogFragmen private lateinit var clearButton: Button private lateinit var refreshButton: Button private lateinit var shareButton: Button - private lateinit var progressBar: ProgressBar private lateinit var recyclerView: RecyclerView private val handler = Handler(Looper.getMainLooper()) private val timeoutRunnable = Runnable { + + setLoadingState(false) + if (isAdded) { setLoadingState(false) Toast.makeText(requireContext(), "Timeout waiting for vehicle response", Toast.LENGTH_SHORT).show() @@ -268,6 +269,8 @@ internal class DiagnosticTroubleCodePreferenceDialogFragment : CoreDialogFragmen super.onPause() requireContext().unregisterReceiver(dtcNotificationsReceiver) handler.removeCallbacks(timeoutRunnable) + + setLoadingState(false) } private fun handleDTCChangedNotification() { From 9042f36e84dfa36b0d32f5b812356c5161fec7af Mon Sep 17 00:00:00 2001 From: Tomek Zebrowski Date: Mon, 23 Mar 2026 14:34:31 +0100 Subject: [PATCH 3/4] feat: add cancel button to screen lock window --- .../obd/graphs/activity/ScreenLockManager.kt | 96 ++++++++++--------- .../main/res/layout/dialog_screen_lock.xml | 41 +++++--- 2 files changed, 83 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/org/obd/graphs/activity/ScreenLockManager.kt b/app/src/main/java/org/obd/graphs/activity/ScreenLockManager.kt index 1bffb464..777372e1 100644 --- a/app/src/main/java/org/obd/graphs/activity/ScreenLockManager.kt +++ b/app/src/main/java/org/obd/graphs/activity/ScreenLockManager.kt @@ -14,52 +14,62 @@ * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ -package org.obd.graphs.activity + package org.obd.graphs.activity -import android.app.Activity -import android.view.View -import android.widget.TextView -import androidx.appcompat.app.AlertDialog -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import org.obd.graphs.R + import android.app.Activity + import android.view.View + import android.widget.Button + import android.widget.TextView + import androidx.appcompat.app.AlertDialog + import androidx.lifecycle.DefaultLifecycleObserver + import androidx.lifecycle.LifecycleOwner + import org.obd.graphs.R -class ScreenLockManager( - private val activity: Activity, -) : DefaultLifecycleObserver { - private var lockScreenDialog: AlertDialog? = null + class ScreenLockManager(private val activity: Activity) : DefaultLifecycleObserver { + private var lockScreenDialog: AlertDialog? = null + private var onCancelAction: (() -> Unit)? = null - fun setup() { - AlertDialog.Builder(activity).run { - setCancelable(false) - val dialogView: View = activity.layoutInflater.inflate(R.layout.dialog_screen_lock, null) - setView(dialogView) - lockScreenDialog = create() - } - } + fun setup() { + AlertDialog.Builder(activity).run { + setCancelable(false) + val dialogView: View = activity.layoutInflater.inflate(R.layout.dialog_screen_lock, null) - fun show(message: String) { - lockScreenDialog?.let { dialog -> - val dialogTitle = dialog.findViewById(R.id.dialog_screen_lock_message_id) - if (dialogTitle != null && message.isNotEmpty()) { - dialogTitle.text = message - } - if (!dialog.isShowing) { - dialog.show() - } - } - } - fun dismiss() { - if (lockScreenDialog?.isShowing == true) { - lockScreenDialog?.dismiss() - } - } + val cancelButton = dialogView.findViewById