// SPDX-License-Identifier: GPL-2.0 #include #include #include #include #include "core/btdiscovery.h" #include #include "ui_btdeviceselectiondialog.h" #include "btdeviceselectiondialog.h" #if defined(Q_OS_WIN) Q_DECLARE_METATYPE(QBluetoothDeviceDiscoveryAgent::Error) #endif #if QT_VERSION < 0x050500 Q_DECLARE_METATYPE(QBluetoothDeviceInfo) #endif BtDeviceSelectionDialog::BtDeviceSelectionDialog(QWidget *parent) : QDialog(parent), ui(new Ui::BtDeviceSelectionDialog), remoteDeviceDiscoveryAgent(0) { ui->setupUi(this); // Quit button callbacks QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); connect(quit, SIGNAL(activated()), this, SLOT(reject())); connect(ui->quit, SIGNAL(clicked()), this, SLOT(reject())); // Translate the UI labels ui->localDeviceDetails->setTitle(tr("Local Bluetooth device details")); ui->selectDeviceLabel->setText(tr("Select device:")); ui->deviceAddressLabel->setText(tr("Address:")); ui->deviceNameLabel->setText(tr("Name:")); ui->deviceState->setText(tr("Bluetooth powered on")); ui->changeDeviceState->setText(tr("Turn on/off")); ui->discoveredDevicesLabel->setText(tr("Discovered devices")); ui->scan->setText(tr("Scan")); ui->clear->setText(tr("Clear")); ui->save->setText(tr("Save")); ui->quit->setText(tr("Quit")); // Disable the save button because there is no device selected ui->save->setEnabled(false); // Add event for item selection connect(ui->discoveredDevicesList, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), this, SLOT(currentItemChanged(QListWidgetItem*,QListWidgetItem*))); #if defined(Q_OS_WIN) ULONG ulRetCode = SUCCESS; WSADATA WSAData = { 0 }; // Initialize WinSock and ask for version 2.2. ulRetCode = WSAStartup(MAKEWORD(2, 2), &WSAData); if (ulRetCode != SUCCESS) { QMessageBox::critical(this, "Bluetooth", tr("Could not initialize Winsock version 2.2"), QMessageBox::Ok); return; } // Initialize the device discovery agent initializeDeviceDiscoveryAgent(); // On Windows we cannot select a device or show information about the local device ui->localDeviceDetails->hide(); #else // Initialize the local Bluetooth device localDevice = new QBluetoothLocalDevice(); // Populate the list with local bluetooth devices QList localAvailableDevices = localDevice->allDevices(); int availableDevicesSize = localAvailableDevices.size(); if (availableDevicesSize > 1) { int defaultDeviceIndex = -1; for (int it = 0; it < availableDevicesSize; it++) { QBluetoothHostInfo localAvailableDevice = localAvailableDevices.at(it); ui->localSelectedDevice->addItem(localAvailableDevice.name(), QVariant::fromValue(localAvailableDevice.address())); if (localDevice->address() == localAvailableDevice.address()) defaultDeviceIndex = it; } // Positionate the current index to the default device and register to index changes events ui->localSelectedDevice->setCurrentIndex(defaultDeviceIndex); connect(ui->localSelectedDevice, SIGNAL(currentIndexChanged(int)), this, SLOT(localDeviceChanged(int))); } else { // If there is only one local Bluetooth adapter hide the combobox and the label ui->selectDeviceLabel->hide(); ui->localSelectedDevice->hide(); } // Update the UI information about the local device updateLocalDeviceInformation(); // Initialize the device discovery agent if (localDevice->isValid()) initializeDeviceDiscoveryAgent(); #endif } BtDeviceSelectionDialog::~BtDeviceSelectionDialog() { delete ui; #if defined(Q_OS_WIN) // Terminate the use of Winsock 2 DLL WSACleanup(); #else // Clean the local device delete localDevice; #endif if (remoteDeviceDiscoveryAgent) { // Clean the device discovery agent if (remoteDeviceDiscoveryAgent->isActive()) { remoteDeviceDiscoveryAgent->stop(); #if defined(Q_OS_WIN) remoteDeviceDiscoveryAgent->wait(); #endif } delete remoteDeviceDiscoveryAgent; } } void BtDeviceSelectionDialog::on_changeDeviceState_clicked() { #if defined(Q_OS_WIN) // TODO add implementation #else if (localDevice->hostMode() == QBluetoothLocalDevice::HostPoweredOff) { ui->dialogStatus->setText(tr("Trying to turn on the local Bluetooth device...")); localDevice->powerOn(); } else { ui->dialogStatus->setText(tr("Trying to turn off the local Bluetooth device...")); localDevice->setHostMode(QBluetoothLocalDevice::HostPoweredOff); } #endif } void BtDeviceSelectionDialog::on_save_clicked() { // Get the selected device. There will be always a selected device if the save button is enabled. QListWidgetItem *currentItem = ui->discoveredDevicesList->currentItem(); QBluetoothDeviceInfo remoteDeviceInfo = currentItem->data(Qt::UserRole).value(); // Save the selected device selectedRemoteDeviceInfo = QSharedPointer(new QBluetoothDeviceInfo(remoteDeviceInfo)); QString address = remoteDeviceInfo.address().isNull() ? remoteDeviceInfo.deviceUuid().toString() : remoteDeviceInfo.address().toString(); saveBtDeviceInfo(address.toUtf8().constData(), remoteDeviceInfo); if (remoteDeviceDiscoveryAgent->isActive()) { // Stop the SDP agent if the clear button is pressed and enable the Scan button remoteDeviceDiscoveryAgent->stop(); #if defined(Q_OS_WIN) remoteDeviceDiscoveryAgent->wait(); #endif ui->scan->setEnabled(true); } // Close the device selection dialog and set the result code to Accepted accept(); } void BtDeviceSelectionDialog::on_clear_clicked() { ui->dialogStatus->setText(tr("Remote devices list was cleared.")); ui->discoveredDevicesList->clear(); if (remoteDeviceDiscoveryAgent->isActive()) { // Stop the SDP agent if the clear button is pressed and enable the Scan button remoteDeviceDiscoveryAgent->stop(); #if defined(Q_OS_WIN) remoteDeviceDiscoveryAgent->wait(); #endif ui->scan->setEnabled(true); } } void BtDeviceSelectionDialog::on_scan_clicked() { ui->dialogStatus->setText(tr("Scanning for remote devices...")); ui->discoveredDevicesList->clear(); remoteDeviceDiscoveryAgent->start(); ui->scan->setEnabled(false); } void BtDeviceSelectionDialog::remoteDeviceScanFinished() { if (remoteDeviceDiscoveryAgent->error() == QBluetoothDeviceDiscoveryAgent::NoError) { ui->dialogStatus->setText(tr("Scanning finished successfully.")); } else { deviceDiscoveryError(remoteDeviceDiscoveryAgent->error()); } ui->scan->setEnabled(true); } void BtDeviceSelectionDialog::hostModeStateChanged(QBluetoothLocalDevice::HostMode mode) { #if defined(Q_OS_WIN) // TODO add implementation #else bool on = !(mode == QBluetoothLocalDevice::HostPoweredOff); //: %1 will be replaced with "turned on" or "turned off" ui->dialogStatus->setText(tr("The local Bluetooth device was %1.") .arg(on? tr("turned on") : tr("turned off"))); ui->deviceState->setChecked(on); ui->scan->setEnabled(on); #endif } void BtDeviceSelectionDialog::addRemoteDevice(const QBluetoothDeviceInfo &remoteDeviceInfo) { #if defined(Q_OS_WIN) // On Windows we cannot obtain the pairing status so we set only the name and the address of the device QString deviceLabel = QString("%1 (%2)").arg(remoteDeviceInfo.name(), remoteDeviceInfo.address().toString()); QColor pairingColor = QColor(Qt::white); #else // By default we use the status label and the color for the UNPAIRED state QColor pairingColor = QColor("#F1A9A0"); QString pairingStatusLabel = tr("UNPAIRED"); QBluetoothLocalDevice::Pairing pairingStatus = localDevice->pairingStatus(remoteDeviceInfo.address()); if (pairingStatus == QBluetoothLocalDevice::Paired) { pairingStatusLabel = tr("PAIRED"); pairingColor = QColor(Qt::gray); } else if (pairingStatus == QBluetoothLocalDevice::AuthorizedPaired) { pairingStatusLabel = tr("AUTHORIZED_PAIRED"); pairingColor = QColor("#89C4F4"); } if (remoteDeviceInfo.address().isNull()) pairingColor = QColor(Qt::gray); QString deviceLabel; #if defined(Q_OS_MACOS) || defined(Q_OS_IOS) if (!remoteDeviceInfo.deviceUuid().isNull()) { // we have only a Uuid, no address, so show that and reset the pairing color deviceLabel = QString("%1 (%2)").arg(remoteDeviceInfo.name(),remoteDeviceInfo.deviceUuid().toString()); pairingColor = QColor(Qt::white); } else #endif deviceLabel = tr("%1 (%2) [State: %3]").arg(remoteDeviceInfo.name(), remoteDeviceInfo.address().toString(), pairingStatusLabel); #endif // Create the new item, set its information and add it to the list QListWidgetItem *item = new QListWidgetItem(deviceLabel); item->setData(Qt::UserRole, QVariant::fromValue(remoteDeviceInfo)); item->setBackgroundColor(pairingColor); ui->discoveredDevicesList->addItem(item); } void BtDeviceSelectionDialog::currentItemChanged(QListWidgetItem *item, QListWidgetItem *) { // If the list is cleared, we get a signal with a null item pointer if (!item) { ui->save->setEnabled(false); return; } // By default we assume that the devices are paired QBluetoothDeviceInfo remoteDeviceInfo = item->data(Qt::UserRole).value(); QString statusMessage = tr("The device %1 can be used for connection. You can press the Save button.") .arg(remoteDeviceInfo.address().isNull() ? remoteDeviceInfo.deviceUuid().toString() : remoteDeviceInfo.address().toString()); bool enableSaveButton = true; #if !defined(Q_OS_WIN) // On other platforms than Windows we can obtain the pairing status so if the devices are not paired we disable the button // except on MacOS for those devices that only give us a Uuid and not and address (as we have no pairing status for those, either) if (!remoteDeviceInfo.address().isNull()) { QBluetoothLocalDevice::Pairing pairingStatus = localDevice->pairingStatus(remoteDeviceInfo.address()); if (pairingStatus == QBluetoothLocalDevice::Unpaired) { statusMessage = tr("The device %1 must be paired in order to be used. Please use the context menu for pairing options.") .arg(remoteDeviceInfo.address().toString()); enableSaveButton = false; } } if (remoteDeviceInfo.address().isNull() && remoteDeviceInfo.deviceUuid().isNull()) { statusMessage = tr("A device needs a non-zero address for a connection."); enableSaveButton = false; } #endif // Update the status message and the save button ui->dialogStatus->setText(statusMessage); ui->save->setEnabled(enableSaveButton); } void BtDeviceSelectionDialog::localDeviceChanged(int index) { #if defined(Q_OS_WIN) // TODO add implementation #else QBluetoothAddress localDeviceSelectedAddress = ui->localSelectedDevice->itemData(index, Qt::UserRole).value(); // Delete the old localDevice if (localDevice) delete localDevice; // Create a new local device using the selected address localDevice = new QBluetoothLocalDevice(localDeviceSelectedAddress); ui->dialogStatus->setText(tr("The local device was changed.")); // Clear the discovered devices list on_clear_clicked(); // Update the UI information about the local device updateLocalDeviceInformation(); // Initialize the device discovery agent if (localDevice->isValid()) initializeDeviceDiscoveryAgent(); #endif } void BtDeviceSelectionDialog::displayPairingMenu(const QPoint &pos) { #if defined(Q_OS_WIN) // TODO add implementation #else QMenu menu(this); QAction *pairAction = menu.addAction(tr("Pair")); QAction *removePairAction = menu.addAction(tr("Remove pairing")); QAction *chosenAction = menu.exec(ui->discoveredDevicesList->viewport()->mapToGlobal(pos)); QListWidgetItem *currentItem = ui->discoveredDevicesList->currentItem(); QBluetoothDeviceInfo currentRemoteDeviceInfo = currentItem->data(Qt::UserRole).value(); QBluetoothLocalDevice::Pairing pairingStatus = localDevice->pairingStatus(currentRemoteDeviceInfo.address()); //TODO: disable the actions if (pairingStatus == QBluetoothLocalDevice::Unpaired) { pairAction->setEnabled(true); removePairAction->setEnabled(false); } else { pairAction->setEnabled(false); removePairAction->setEnabled(true); } if (chosenAction == pairAction) { ui->dialogStatus->setText(tr("Trying to pair device %1") .arg(currentRemoteDeviceInfo.address().toString())); localDevice->requestPairing(currentRemoteDeviceInfo.address(), QBluetoothLocalDevice::Paired); } else if (chosenAction == removePairAction) { ui->dialogStatus->setText(tr("Trying to unpair device %1") .arg(currentRemoteDeviceInfo.address().toString())); localDevice->requestPairing(currentRemoteDeviceInfo.address(), QBluetoothLocalDevice::Unpaired); } #endif } void BtDeviceSelectionDialog::pairingFinished(const QBluetoothAddress &address, QBluetoothLocalDevice::Pairing pairing) { // Determine the color, the new pairing status and the log message. By default we assume that the devices are UNPAIRED. QString remoteDeviceStringAddress = address.toString(); QColor pairingColor = QColor(Qt::red); QString pairingStatusLabel = tr("UNPAIRED"); QString dialogStatusMessage = tr("Device %1 was unpaired.").arg(remoteDeviceStringAddress); bool enableSaveButton = false; if (pairing == QBluetoothLocalDevice::Paired) { pairingStatusLabel = tr("PAIRED"); pairingColor = QColor(Qt::gray); enableSaveButton = true; dialogStatusMessage = tr("Device %1 was paired.").arg(remoteDeviceStringAddress); } else if (pairing == QBluetoothLocalDevice::AuthorizedPaired) { pairingStatusLabel = tr("AUTHORIZED_PAIRED"); pairingColor = QColor(Qt::blue); enableSaveButton = true; dialogStatusMessage = tr("Device %1 was paired and is authorized.").arg(remoteDeviceStringAddress); } // Find the items which represent the BTH device and update their state QList items = ui->discoveredDevicesList->findItems(remoteDeviceStringAddress, Qt::MatchContains); QRegularExpression pairingExpression = QRegularExpression(QString("%1|%2|%3").arg(tr("PAIRED"), tr("AUTHORIZED_PAIRED"), tr("UNPAIRED"))); for (int i = 0; i < items.count(); ++i) { QListWidgetItem *item = items.at(i); QString updatedDeviceLabel = item->text().replace(QRegularExpression(pairingExpression), pairingStatusLabel); item->setText(updatedDeviceLabel); item->setBackgroundColor(pairingColor); } // Check if the updated device is the selected one from the list and inform the user that it can/cannot start the download mode QListWidgetItem *currentItem = ui->discoveredDevicesList->currentItem(); if (currentItem != NULL && currentItem->text().contains(remoteDeviceStringAddress, Qt::CaseInsensitive)) { if (pairing == QBluetoothLocalDevice::Unpaired) { dialogStatusMessage = tr("The device %1 must be paired in order to be used. Please use the context menu for pairing options.") .arg(remoteDeviceStringAddress); } else { dialogStatusMessage = tr("The device %1 can now be used for connection. You can press the Save button.") .arg(remoteDeviceStringAddress); } } // Update the save button and the dialog status message ui->save->setEnabled(enableSaveButton); ui->dialogStatus->setText(dialogStatusMessage); } void BtDeviceSelectionDialog::error(QBluetoothLocalDevice::Error error) { ui->dialogStatus->setText(tr("Local device error: %1.") .arg((error == QBluetoothLocalDevice::PairingError)? tr("Pairing error. If the remote device requires a custom PIN code, " "please try to pair the devices using your operating system. ") : tr("Unknown error"))); } void BtDeviceSelectionDialog::deviceDiscoveryError(QBluetoothDeviceDiscoveryAgent::Error error) { QString errorDescription; switch (error) { case QBluetoothDeviceDiscoveryAgent::PoweredOffError: errorDescription = tr("The Bluetooth adaptor is powered off, power it on before doing discovery."); break; case QBluetoothDeviceDiscoveryAgent::InputOutputError: errorDescription = tr("Writing to or reading from the device resulted in an error."); break; default: #if defined(Q_OS_WIN) errorDescription = remoteDeviceDiscoveryAgent->errorToString(); #else errorDescription = tr("An unknown error has occurred."); #endif break; } ui->dialogStatus->setText(tr("Device discovery error: %1.").arg(errorDescription)); } extern QString markBLEAddress(const QBluetoothDeviceInfo *device); QString BtDeviceSelectionDialog::getSelectedDeviceAddress() { if (selectedRemoteDeviceInfo) return markBLEAddress(selectedRemoteDeviceInfo.data()); return QString(); } QString BtDeviceSelectionDialog::getSelectedDeviceName() { if (selectedRemoteDeviceInfo) { return selectedRemoteDeviceInfo.data()->name(); } return QString(); } void BtDeviceSelectionDialog::updateLocalDeviceInformation() { #if defined(Q_OS_WIN) // TODO add implementation #else // Check if the selected Bluetooth device can be accessed if (!localDevice->isValid()) { QString na = tr("Not available"); // Update the UI information ui->deviceAddress->setText(na); ui->deviceName->setText(na); // Announce the user that there is a problem with the selected local Bluetooth adapter ui->dialogStatus->setText(tr("The local Bluetooth adapter cannot be accessed.")); // Disable the buttons ui->save->setEnabled(false); ui->scan->setEnabled(false); ui->clear->setEnabled(false); ui->changeDeviceState->setEnabled(false); return; } // Set UI information about the local device ui->deviceAddress->setText(localDevice->address().toString()); ui->deviceName->setText(localDevice->name()); connect(localDevice, SIGNAL(hostModeStateChanged(QBluetoothLocalDevice::HostMode)), this, SLOT(hostModeStateChanged(QBluetoothLocalDevice::HostMode))); // Initialize the state of the local device and activate/deactive the scan button hostModeStateChanged(localDevice->hostMode()); // Add context menu for devices to be able to pair them ui->discoveredDevicesList->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->discoveredDevicesList, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(displayPairingMenu(QPoint))); connect(localDevice, SIGNAL(pairingFinished(QBluetoothAddress, QBluetoothLocalDevice::Pairing)), this, SLOT(pairingFinished(QBluetoothAddress, QBluetoothLocalDevice::Pairing))); connect(localDevice, SIGNAL(error(QBluetoothLocalDevice::Error)), this, SLOT(error(QBluetoothLocalDevice::Error))); #endif } void BtDeviceSelectionDialog::initializeDeviceDiscoveryAgent() { #if defined(Q_OS_WIN) // Register QBluetoothDeviceInfo metatype qRegisterMetaType(); // Register QBluetoothDeviceDiscoveryAgent metatype (Needed for QBluetoothDeviceDiscoveryAgent::Error) qRegisterMetaType(); // Intialize the discovery agent remoteDeviceDiscoveryAgent = new WinBluetoothDeviceDiscoveryAgent(this); #else // Intialize the discovery agent remoteDeviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(localDevice->address()); // Test if the discovery agent was successfully created if (remoteDeviceDiscoveryAgent->error() == QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError) { ui->dialogStatus->setText(tr("The device discovery agent was not created because the %1 address does not " "match the physical adapter address of any local Bluetooth device.") .arg(localDevice->address().toString())); ui->scan->setEnabled(false); ui->clear->setEnabled(false); return; } #endif connect(remoteDeviceDiscoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)), this, SLOT(addRemoteDevice(QBluetoothDeviceInfo))); connect(remoteDeviceDiscoveryAgent, SIGNAL(finished()), this, SLOT(remoteDeviceScanFinished())); connect(remoteDeviceDiscoveryAgent, SIGNAL(error(QBluetoothDeviceDiscoveryAgent::Error)), this, SLOT(deviceDiscoveryError(QBluetoothDeviceDiscoveryAgent::Error))); } #if defined(Q_OS_WIN) WinBluetoothDeviceDiscoveryAgent::WinBluetoothDeviceDiscoveryAgent(QObject *parent) : QThread(parent) { // Initialize the internal flags by their default values running = false; stopped = false; lastError = QBluetoothDeviceDiscoveryAgent::NoError; lastErrorToString = tr("No error"); } WinBluetoothDeviceDiscoveryAgent::~WinBluetoothDeviceDiscoveryAgent() { } bool WinBluetoothDeviceDiscoveryAgent::isActive() const { return running; } QString WinBluetoothDeviceDiscoveryAgent::errorToString() const { return lastErrorToString; } QBluetoothDeviceDiscoveryAgent::Error WinBluetoothDeviceDiscoveryAgent::error() const { return lastError; } void WinBluetoothDeviceDiscoveryAgent::run() { // Initialize query for device and start the lookup service WSAQUERYSET queryset; HANDLE hLookup; int result = SUCCESS; running = true; lastError = QBluetoothDeviceDiscoveryAgent::NoError; lastErrorToString = tr("No error"); memset(&queryset, 0, sizeof(WSAQUERYSET)); queryset.dwSize = sizeof(WSAQUERYSET); queryset.dwNameSpace = NS_BTH; // The LUP_CONTAINERS flag is used to signal that we are doing a device inquiry // while LUP_FLUSHCACHE flag is used to flush the device cache for all inquiries // and to do a fresh lookup instead. result = WSALookupServiceBegin(&queryset, LUP_CONTAINERS | LUP_FLUSHCACHE, &hLookup); if (result != SUCCESS) { // Get the last error and emit a signal lastErrorToString = qt_error_string(); lastError = QBluetoothDeviceDiscoveryAgent::PoweredOffError; emit error(lastError); // Announce that the inquiry finished and restore the stopped flag running = false; stopped = false; return; } // Declare the necessary variables to collect the information BYTE buffer[4096]; DWORD bufferLength = sizeof(buffer); WSAQUERYSET *pResults = (WSAQUERYSET*)&buffer; memset(buffer, 0, sizeof(buffer)); pResults->dwSize = sizeof(WSAQUERYSET); pResults->dwNameSpace = NS_BTH; pResults->lpBlob = NULL; //Start looking for devices while (result == SUCCESS && !stopped){ // LUP_RETURN_NAME and LUP_RETURN_ADDR flags are used to return the name and the address of the discovered device result = WSALookupServiceNext(hLookup, LUP_RETURN_NAME | LUP_RETURN_ADDR, &bufferLength, pResults); if (result == SUCCESS) { // Found a device QString deviceAddress(BTH_ADDR_BUF_LEN, Qt::Uninitialized); DWORD addressSize = BTH_ADDR_BUF_LEN; // Collect the address of the device from the WSAQUERYSET SOCKADDR_BTH *socketBthAddress = (SOCKADDR_BTH *) pResults->lpcsaBuffer->RemoteAddr.lpSockaddr; // Convert the BTH_ADDR to string if (WSAAddressToStringW((LPSOCKADDR) socketBthAddress, sizeof (*socketBthAddress), NULL, reinterpret_cast(deviceAddress.data()), &addressSize ) != 0) { // Get the last error and emit a signal lastErrorToString = qt_error_string(); lastError = QBluetoothDeviceDiscoveryAgent::UnknownError; emit(lastError); break; } // Remove the round parentheses deviceAddress.remove(')'); deviceAddress.remove('('); // Save the name of the discovered device and truncate the address QString deviceName = QString(pResults->lpszServiceInstanceName); deviceAddress.truncate(BTH_ADDR_PRETTY_STRING_LEN); // Create an object with information about the discovered device QBluetoothDeviceInfo deviceInfo = QBluetoothDeviceInfo(QBluetoothAddress(deviceAddress), deviceName, 0); // Raise a signal with information about the found remote device emit deviceDiscovered(deviceInfo); } else { // Get the last error and emit a signal lastErrorToString = qt_error_string(); lastError = QBluetoothDeviceDiscoveryAgent::UnknownError; emit(lastError); } } // Announce that the inquiry finished and restore the stopped flag running = false; stopped = false; // Restore the error status lastError = QBluetoothDeviceDiscoveryAgent::NoError; // End the lookup service WSALookupServiceEnd(hLookup); } void WinBluetoothDeviceDiscoveryAgent::stop() { // Stop the inqury stopped = true; } #endif