From 5fd45d1d9a08271ab8279101235edb2b0fea2743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sat, 17 Jan 2026 14:31:45 +0000 Subject: [PATCH] Debug: Allow setting device type --- .../activities/debug/DeviceDebugFragment.kt | 69 +++- .../activities/debug/MainDebugFragment.kt | 6 +- .../discovery/DiscoveryActivityV2.java | 6 +- .../externalevents/IntentApiReceiver.java | 4 +- .../gadgetbridge/util/DeviceTypeDialog.kt | 339 ++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 6 files changed, 416 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceTypeDialog.kt diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DeviceDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DeviceDebugFragment.kt index a5188e4a41..957c0f83b1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DeviceDebugFragment.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/DeviceDebugFragment.kt @@ -1,13 +1,23 @@ package nodomain.freeyourgadget.gadgetbridge.activities.debug +import android.content.DialogInterface import android.os.Bundle +import android.widget.Toast +import androidx.preference.EditTextPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import nodomain.freeyourgadget.gadgetbridge.GBApplication import nodomain.freeyourgadget.gadgetbridge.R +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType +import nodomain.freeyourgadget.gadgetbridge.util.DeviceTypeDialog +import nodomain.freeyourgadget.gadgetbridge.util.GB import org.slf4j.Logger import org.slf4j.LoggerFactory + class DeviceDebugFragment : AbstractDebugFragment() { private lateinit var gbDevice: GBDevice @@ -24,12 +34,29 @@ class DeviceDebugFragment : AbstractDebugFragment() { preferenceScreen?.title = gbDevice.aliasOrName findPreference(PREF_DEBUG_DEVICE_NAME)?.summary = gbDevice.name ?: "" - findPreference(PREF_DEBUG_DEVICE_ALIAS)?.summary = gbDevice.alias ?: getString(R.string.not_set) + if (gbDevice.alias.isNullOrEmpty()) { + findPreference(PREF_DEBUG_DEVICE_ALIAS)?.summary = getString(R.string.not_set) + } else { + findPreference(PREF_DEBUG_DEVICE_ALIAS)?.summary = gbDevice.alias ?: getString(R.string.not_set) + } findPreference(PREF_DEBUG_DEVICE_MAC_ADDRESS)?.summary = gbDevice.address + + // Device Type findPreference(PREF_DEBUG_DEVICE_TYPE)?.summary = gbDevice.type.name + findPreference(PREF_DEBUG_DEVICE_TYPE)?.setOnPreferenceClickListener { + DeviceTypeDialog(requireActivity(), R.string.set_device_type, gbDevice.address) + .show(gbDevice.type) { _: String, deviceType: DeviceType -> + setDeviceType(gbDevice, deviceType) + } + true + } val headerDetails = findPreference(PREF_HEADER_DETAILS) for (detail in gbDevice.deviceInfos) { + if (detail.name.startsWith("ADDR:")) { + // We already show the address above + continue + } addDynamicPref(headerDetails, detail.name.replace(": *$".toRegex(), ""), detail.details) } @@ -59,6 +86,46 @@ class DeviceDebugFragment : AbstractDebugFragment() { } } + private fun setDeviceType(gbDevice: GBDevice, newType: DeviceType) { + LOG.debug("Setting device {} type from {} to {}", gbDevice.address, gbDevice.type, newType) + + if (gbDevice.type == newType) { + GB.toast("Type is the same!", Toast.LENGTH_SHORT, GB.INFO) + return + } + + try { + GBApplication.acquireDB().use { dbHandler -> + val session = dbHandler.getDaoSession() + DBHelper.updateDeviceType(session, gbDevice.address, newType) + } + } catch (e: java.lang.Exception) { + LOG.error("Failed to update device type", e) + GB.toast("Failed to update device type", Toast.LENGTH_LONG, GB.ERROR) + return + } + + LOG.info("Restarting GB after device type update") + + restart() + } + + private fun restart() { + MaterialAlertDialogBuilder(requireActivity()) + .setCancelable(false) + .setIcon(R.drawable.ic_sync) + .setTitle(R.string.backup_restore_restart_title) + .setMessage(getString(R.string.backup_restore_restart_summary, getString(R.string.app_name))) + .setOnCancelListener((DialogInterface.OnCancelListener { _: DialogInterface? -> + requireActivity().finishAffinity() + GBApplication.restart() + })) + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> + requireActivity().finishAffinity() + GBApplication.restart() + }.show() + } + companion object { private val LOG: Logger = LoggerFactory.getLogger(DeviceDebugFragment::class.java) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/MainDebugFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/MainDebugFragment.kt index 9c44fd6585..3a3a8fc0e0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/MainDebugFragment.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/debug/MainDebugFragment.kt @@ -16,7 +16,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.welcome.WelcomeActivity import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote import nodomain.freeyourgadget.gadgetbridge.util.FileUtils import nodomain.freeyourgadget.gadgetbridge.util.GB -import nodomain.freeyourgadget.gadgetbridge.util.TestDeviceDialog +import nodomain.freeyourgadget.gadgetbridge.util.DeviceTypeDialog import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.File @@ -31,8 +31,8 @@ class MainDebugFragment : AbstractDebugFragment() { setPreferencesFromResource(R.xml.debug_preferences_main, rootKey) onClick(PREF_DEBUG_ADD_TEST_DEVICE) { - TestDeviceDialog(requireActivity(), null).show { address, deviceType -> - TestDeviceDialog.createTestDevice(requireContext(), deviceType, address, null) + DeviceTypeDialog(requireActivity(), R.string.add_test_device, null).show { address, deviceType -> + DeviceTypeDialog.createTestDevice(requireContext(), deviceType, address, null) } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/discovery/DiscoveryActivityV2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/discovery/DiscoveryActivityV2.java index aa2a6b66f0..ef00dfa24c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/discovery/DiscoveryActivityV2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/discovery/DiscoveryActivityV2.java @@ -95,7 +95,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; -import nodomain.freeyourgadget.gadgetbridge.util.TestDeviceDialog; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceTypeDialog; public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterView.OnItemClickListener, @@ -775,8 +775,8 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi private void showUnsupportedDeviceDialog(final GBDeviceCandidate deviceCandidate) { LOG.info("Unsupported device candidate selected: {}", deviceCandidate); - new TestDeviceDialog(this, deviceCandidate.getMacAddress()) - .show((macAddress, deviceType) -> { + new DeviceTypeDialog(this, R.string.add_test_device, deviceCandidate.getMacAddress()) + .show(null, (macAddress, deviceType) -> { LOG.debug("Force-pairing {} as {}", deviceCandidate, deviceType); DeviceHelper.getInstance().setForcedDeviceType(deviceCandidate.getMacAddress().toLowerCase(), deviceType); preparePair(deviceCandidate); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/IntentApiReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/IntentApiReceiver.java index 5caac27621..acccb1a031 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/IntentApiReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/IntentApiReceiver.java @@ -380,9 +380,9 @@ public class IntentApiReceiver extends BroadcastReceiver { return; } - LOG.info("Quitting GB after device type update"); + LOG.info("Restarting GB after device type update"); - GBApplication.quit(); + GBApplication.restart(); } /** diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceTypeDialog.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceTypeDialog.kt new file mode 100644 index 0000000000..6d114f923c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceTypeDialog.kt @@ -0,0 +1,339 @@ +package nodomain.freeyourgadget.gadgetbridge.util + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.text.Editable +import android.text.InputFilter +import android.text.InputType +import android.text.Spanned +import android.text.TextWatcher +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.Toast +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import nodomain.freeyourgadget.gadgetbridge.BuildConfig +import nodomain.freeyourgadget.gadgetbridge.GBApplication +import nodomain.freeyourgadget.gadgetbridge.R +import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconAdapter +import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconItem +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper +import nodomain.freeyourgadget.gadgetbridge.databinding.DialogTestDeviceBinding +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.Random +import java.util.TreeMap +import kotlin.collections.map + +class DeviceTypeDialog( + private val activity: Activity, + private val dialogTitle: Int, + private val macAddress: String? +) { + var selectedTestDeviceMAC = macAddress ?: randomMac() + var selectedTestDeviceKey = -1L + private lateinit var updateOkButtonState: (() -> Unit) + + fun show(preselectedType: DeviceType? = null, onSelection: (String, DeviceType) -> Unit) { + val binding = DialogTestDeviceBinding.inflate(activity.layoutInflater) + + val allDevicesByName: MutableMap = getAllSupportedDevices(activity) + + // Get unique manufacturers + val manufacturerMap = mutableMapOf>>() + val manufacturers = mutableSetOf() + for (entry in allDevicesByName.entries) { + val manufacturer = entry.value.deviceType.getDeviceCoordinator().getManufacturer() + manufacturers.add(manufacturer) + manufacturerMap.getOrPut(manufacturer) { mutableListOf() }.add(entry) + } + + val manufacturerPriority = listOf( + "Gadgetbridge", + activity.getString(R.string.pref_header_generic), + activity.getString(R.string.unknown) + ) + val sortedManufacturers = manufacturers.toList().sortedWith( + compareBy { item -> + val index = manufacturerPriority.indexOfFirst { it.equals(item, ignoreCase = true) } + if (index >= 0) index else Int.MAX_VALUE + }.thenBy(String.CASE_INSENSITIVE_ORDER) { it } + ) + val manufacturerListArray = ArrayList() + manufacturerListArray.add(activity.getString(R.string.all_manufacturers)) + manufacturerListArray.addAll(sortedManufacturers) + + // Create full device list + val allDeviceListArray = allDevicesByName.entries + .map { SpinnerWithIconItem(it.key, it.value.deviceType.ordinal.toLong(), it.value.icon) } + .toCollection(ArrayList()) + + val currentDeviceList = ArrayList(allDeviceListArray) + + val deviceListAdapter = SpinnerWithIconAdapter( + activity, + R.layout.spinner_with_image_layout, + R.id.spinner_item_text, + currentDeviceList + ) + binding.deviceTypeDropdown.setAdapter(deviceListAdapter) + binding.deviceTypeDropdown.onItemClickListener = AdapterView.OnItemClickListener { parent, _, position, _ -> + val selectedItem = parent.getItemAtPosition(position) as SpinnerWithIconItem + selectedTestDeviceKey = selectedItem.getId() + updateOkButtonState.invoke() + } + + // Set up manufacturer dropdown + binding.deviceManufacturerDropdown.setAdapter( + ArrayAdapter( + activity, + android.R.layout.simple_dropdown_item_1line, + manufacturerListArray + ) + ) + binding.deviceManufacturerDropdown.setText(activity.getString(R.string.all_manufacturers), false) + binding.deviceManufacturerDropdown.onItemClickListener = + AdapterView.OnItemClickListener { parent, _, position, _ -> + val selectedManufacturer = parent.getItemAtPosition(position) as String + + // Filter device list based on selected manufacturer + currentDeviceList.clear() + if (position == 0) { + // Show all devices + currentDeviceList.addAll(allDeviceListArray) + } else { + // Show only devices from selected manufacturer + val filteredDevices = getFilteredDevices(selectedManufacturer, manufacturerMap) + currentDeviceList.addAll(filteredDevices) + } + deviceListAdapter.notifyDataSetChanged() + + // Reset device selection + binding.deviceTypeDropdown.setText("", false) + selectedTestDeviceKey = -1L + updateOkButtonState.invoke() + } + + val dialog = MaterialAlertDialogBuilder(activity) + .setCancelable(true) + .setTitle(dialogTitle) + .setView(binding.root) + .setPositiveButton(R.string.ok) { _, _ -> + onSelection.invoke( + binding.macEditText.text.toString(), + DeviceType.entries[selectedTestDeviceKey.toInt()] + ) + } + .setNegativeButton(R.string.Cancel) { _, _ -> } + .create() + + // Validate and update OK button state + updateOkButtonState = { + val positiveButton = dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE) + val isMacValid = isValidMacAddress(binding.macEditText.text.toString()) + val isDeviceSelected = selectedTestDeviceKey != -1L + val typeIsDifferentFromDefault = + preselectedType == null || preselectedType.ordinal.toLong() != selectedTestDeviceKey + positiveButton?.isEnabled = isMacValid && isDeviceSelected && typeIsDifferentFromDefault + } + + dialog.setOnShowListener { + updateOkButtonState.invoke() + } + + setupMacAddressInput(binding.macEditText) + + // Preselect manufacturer and device type if provided + preselectedType?.let { deviceType -> + val coordinator = deviceType.getDeviceCoordinator() + val manufacturer = when (coordinator.getManufacturer()) { + "Unknown" -> activity.getString(R.string.unknown) + "Generic" -> activity.getString(R.string.pref_header_generic) + else -> coordinator.getManufacturer() + } + + // Set manufacturer dropdown + if (manufacturer in sortedManufacturers) { + binding.deviceManufacturerDropdown.setText(manufacturer, false) + + // Filter device list based on selected manufacturer + currentDeviceList.clear() + val filteredDevices = getFilteredDevices(manufacturer, manufacturerMap) + currentDeviceList.addAll(filteredDevices) + deviceListAdapter.notifyDataSetChanged() + } + + // Find and set the device type in the list + val matchingItem = currentDeviceList.find { it.getId() == deviceType.ordinal.toLong() } + matchingItem?.let { + binding.deviceTypeDropdown.setText(it.getText(), false) + selectedTestDeviceKey = it.getId() + updateOkButtonState.invoke() + } + } + + dialog.show() + } + + private fun getFilteredDevices( + manufacturer: String, + manufacturerMap: Map>> + ): List { + return manufacturerMap[manufacturer]?.map { + SpinnerWithIconItem( + it.key.replace("^${manufacturer} ".toRegex(), "") + .replace(" \\(${manufacturer}\\)$".toRegex(), ""), + it.value.deviceType.ordinal.toLong(), + it.value.icon + ) + } ?: emptyList() + } + + private fun getAllSupportedDevices(context: Context): MutableMap { + var newMap = LinkedHashMap(1) + for (deviceType in DeviceType.entries) { + val coordinator = deviceType.getDeviceCoordinator() + val icon = coordinator.getDefaultIconResource() + var name = context.getString(coordinator.getDeviceNameResource()) + val manufacturer = when (coordinator.getManufacturer()) { + "Unknown" -> context.getString(R.string.unknown) + "Generic" -> context.getString(R.string.pref_header_generic) + else -> coordinator.getManufacturer() + } + if (!name.startsWith(manufacturer)) { + name += " (${manufacturer})" + } + newMap[name] = DeviceTypeWithIcon(deviceType, icon) + } + + val sortedMap = TreeMap(String.CASE_INSENSITIVE_ORDER) + sortedMap.putAll(newMap) + newMap = LinkedHashMap(sortedMap.size + 3) + + // Ensure some devices are first + //newMap[context.getString(R.string.devicetype_scannable)] = + // DeviceTypeWithIcon(DeviceType.SCANNABLE, R.drawable.ic_device_scannable) + //newMap[context.getString(R.string.devicetype_ble_gatt_client)] = + // DeviceTypeWithIcon(DeviceType.BLE_GATT_CLIENT, R.drawable.ic_device_scannable) + + newMap.putAll(sortedMap) + + return newMap + } + + private fun setupMacAddressInput(editText: EditText) { + editText.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + + if (macAddress != null) { + editText.isEnabled = false + } + + // help the user to input a properly formated MAC - see BluetoothAdapter.checkBluetoothAddress + // for Bangle.js builds: also support pebble emulator addresses + editText.filters = arrayOf(object : InputFilter { + @Suppress("KotlinConstantConditions") + override fun filter( + source: CharSequence, + start: Int, end: Int, + dest: Spanned, + dstart: Int, dend: Int + ): CharSequence { + val builder = StringBuilder() + for (i in start.. + val daoSession = db.getDaoSession() + val gbDevice = GBDevice(deviceMac, deviceName, "", null, deviceType) + gbDevice.firmwareVersion = "N/A" + gbDevice.firmwareVersion2 = "N/A" + + //this causes the attributes (fw version) to be stored as well. Not much useful, but still... + gbDevice.setState(GBDevice.State.INITIALIZED) + + DBHelper.getDevice(gbDevice, daoSession) //the addition happens here + + val refreshIntent = Intent(DeviceManager.ACTION_REFRESH_DEVICELIST) + LocalBroadcastManager.getInstance(context).sendBroadcast(refreshIntent) + GB.toast(context, "Added test device: $deviceName", Toast.LENGTH_SHORT, GB.INFO) + } + } catch (e: Exception) { + LOG.error("Error accessing database", e) + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 003a94e221..20f55fb9d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1837,6 +1837,7 @@ Unknown Device Test Device Add test device + Set device type Pebble Mi Band Mi Band 2