#include #include "Tactility/Paths.h" #include #include #include #include #include #include #include #include #include namespace tt::bluetooth::settings { constexpr auto* TAG = "BluetoothPairedDevice"; // Use the same directory as the old service for backward compatibility. constexpr auto* DEVICE_SETTINGS_FORMAT = "{}/{}.device.properties"; constexpr auto* KEY_NAME = "name"; constexpr auto* KEY_ADDR = "addr"; constexpr auto* KEY_AUTO_CONNECT = "autoConnect"; constexpr auto* KEY_PROFILE_ID = "profileId"; constexpr auto* KEY_ADDR_TYPE = "addrType"; static std::string getSettingsFilePath() { return getUserDataPath() + "/service/bluetooth"; } std::string addrToHex(const std::array& addr) { std::stringstream stream; stream << std::hex; for (int i = 0; i < 6; ++i) { stream << std::setw(2) << std::setfill('0') << static_cast(addr[i]); } return stream.str(); } static bool hexToAddr(const std::string& hex, std::array& addr) { if (hex.size() != 12) { LOG_E(TAG, "hexToAddr() length mismatch: expected 12, got %d", (int)hex.size()); return false; } char buf[3] = { 0 }; for (int i = 0; i < 6; ++i) { buf[0] = hex[i * 2]; buf[1] = hex[i * 2 + 1]; char* endptr = nullptr; addr[i] = static_cast(strtoul(buf, &endptr, 16)); if (endptr != buf + 2) { LOG_E(TAG, "hexToAddr() invalid hex at byte %d: '%c%c'", i, buf[0], buf[1]); return false; } } return true; } static std::string getFilePath(const std::string& addr_hex) { return std::format(DEVICE_SETTINGS_FORMAT, getSettingsFilePath(), addr_hex); } bool hasFileForDevice(const std::string& addr_hex) { return file::isFile(getFilePath(addr_hex)); } bool load(const std::string& addr_hex, PairedDevice& device) { auto file_path = getFilePath(addr_hex); if (!file::isFile(file_path)) return false; std::map map; if (!file::loadPropertiesFile(file_path, map)) return false; if (!map.contains(KEY_ADDR)) return false; if (!hexToAddr(map[KEY_ADDR], device.addr)) return false; device.name = map.contains(KEY_NAME) ? map[KEY_NAME] : ""; device.autoConnect = !map.contains(KEY_AUTO_CONNECT) || (map[KEY_AUTO_CONNECT] == "true"); if (map.contains(KEY_PROFILE_ID)) { char* endPtr = nullptr; long val = std::strtol(map[KEY_PROFILE_ID].c_str(), &endPtr, 10); if (endPtr != map[KEY_PROFILE_ID].c_str()) { device.profileId = static_cast(val); } } if (map.contains(KEY_ADDR_TYPE)) { char* endPtr = nullptr; long val = std::strtol(map[KEY_ADDR_TYPE].c_str(), &endPtr, 10); if (endPtr != map[KEY_ADDR_TYPE].c_str() && val >= 0 && val <= 255) { device.addrType = static_cast(val); } else { device.addrType = 0; } } else { device.addrType = 0; // backward compat: assume PUBLIC } return true; } bool save(const PairedDevice& device) { const auto addr_hex = addrToHex(device.addr); std::map map; map[KEY_NAME] = device.name; map[KEY_ADDR] = addr_hex; map[KEY_AUTO_CONNECT] = device.autoConnect ? "true" : "false"; map[KEY_PROFILE_ID] = std::to_string(device.profileId); map[KEY_ADDR_TYPE] = std::to_string(device.addrType); auto file_path = getFilePath(addr_hex); LOG_I(TAG, "Saving device file %s profile=%d auto=%d type=%d", file_path.c_str(), device.profileId, (int)device.autoConnect, (int)device.addrType); if (!file::findOrCreateParentDirectory(file_path, 0755)) { LOG_E(TAG, "Failed to create parent dir for %s", file_path.c_str()); return false; } bool result = file::savePropertiesFile(file_path, map); LOG_I(TAG, "SavePropertiesFile result for %s: %d", file_path.c_str(), (int)result); return result; } bool remove(const std::string& addr_hex) { const auto file_path = getFilePath(addr_hex); if (!file::isFile(file_path)) return false; return ::remove(file_path.c_str()) == 0; } std::vector loadAll() { std::vector entries; if (!file::isDirectory(getSettingsFilePath())) { LOG_I(TAG, "loadAll: directory %s does not exist", getSettingsFilePath().c_str()); return {}; } file::scandir(getSettingsFilePath(), entries, [](const dirent* entry) -> int { if (entry->d_type != file::TT_DT_REG && entry->d_type != file::TT_DT_UNKNOWN) return -1; std::string name = entry->d_name; return name.ends_with(".device.properties") ? 0 : -1; }, nullptr); LOG_I(TAG, "loadAll: found %d entries in %s", (int)entries.size(), getSettingsFilePath().c_str()); std::vector result; result.reserve(entries.size()); for (const auto& entry : entries) { std::string filename = entry.d_name; constexpr std::string_view suffix = ".device.properties"; if (filename.size() <= suffix.size()) continue; const std::string addr_hex = filename.substr(0, filename.size() - suffix.size()); PairedDevice device; if (load(addr_hex, device)) { result.push_back(std::move(device)); } else { LOG_W(TAG, "loadAll: failed to load %s", filename.c_str()); } } return result; } } // namespace tt::bluetooth::settings