/* * iDescriptor: A free and open-source idevice management tool. * * Copyright (C) 2025 Uncore * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include "toolboxwidget.h" #include "airplaywindow.h" #include "appcontext.h" #include "cableinfowidget.h" #include "devdiskimageswidget.h" #include "devdiskmanager.h" #include "iDescriptor-ui.h" #include "iDescriptor.h" #ifndef __APPLE__ #include "ifusewidget.h" #endif #include "livescreenwidget.h" #include "querymobilegestaltwidget.h" #include "virtuallocationwidget.h" #include "wirelessgalleryimportwidget.h" #include #include #include #include struct iDescriptorToolWidget { iDescriptorTool tool; QString description; bool requiresDevice; QString iconName; }; bool enterRecoveryMode(iDescriptorDevice *device) { lockdownd_client_t client = NULL; lockdownd_error_t ldret = LOCKDOWN_E_UNKNOWN_ERROR; idevice_error_t ret = IDEVICE_E_UNKNOWN_ERROR; if (LOCKDOWN_E_SUCCESS != (ldret = lockdownd_client_new(device->device, &client, APP_LABEL))) { printf("ERROR: Could not connect to lockdownd: %s (%d)\n", lockdownd_strerror(ldret), ldret); return false; } ldret = lockdownd_enter_recovery(client); if (ldret == LOCKDOWN_E_SESSION_INACTIVE) { lockdownd_client_free(client); client = NULL; if (LOCKDOWN_E_SUCCESS != (ldret = lockdownd_client_new_with_handshake( device->device, &client, APP_LABEL))) { printf("ERROR: Could not connect to lockdownd: %s (%d)\n", lockdownd_strerror(ldret), ldret); return false; } ldret = lockdownd_enter_recovery(client); } lockdownd_client_free(client); if (ldret != LOCKDOWN_E_SUCCESS) { printf("Failed to enter recovery mode.\n"); return false; } else { printf("Device is successfully switching to recovery mode.\n"); return true; } } ToolboxWidget *ToolboxWidget::sharedInstance() { static ToolboxWidget *instance = new ToolboxWidget(); return instance; } ToolboxWidget::ToolboxWidget(QWidget *parent) : QWidget{parent} { setupUI(); updateDeviceList(); updateToolboxStates(); connect(AppContext::sharedInstance(), &AppContext::deviceChange, this, &ToolboxWidget::updateUI); connect(AppContext::sharedInstance(), &AppContext::currentDeviceSelectionChanged, this, &ToolboxWidget::onCurrentDeviceChanged); } void ToolboxWidget::setupUI() { QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(0, 0, 0, 0); // Device selection section QHBoxLayout *deviceLayout = new QHBoxLayout(); m_deviceLabel = new QLabel("Device:"); m_deviceCombo = new QComboBox(); m_deviceCombo->setMinimumWidth(200); deviceLayout->addWidget(m_deviceLabel); deviceLayout->addWidget(m_deviceCombo); deviceLayout->setContentsMargins(15, 5, 15, 5); deviceLayout->addStretch(); mainLayout->addLayout(deviceLayout); // Scroll area for toolboxes m_scrollArea = new QScrollArea(); m_scrollArea->setWidgetResizable(true); m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_scrollArea->setStyleSheet( "QScrollArea { background: transparent; border: none; }"); m_scrollArea->viewport()->setStyleSheet("background: transparent;"); m_contentWidget = new QWidget(); QVBoxLayout *contentLayout = new QVBoxLayout(m_contentWidget); contentLayout->setSpacing(20); contentLayout->setContentsMargins(0, 0, 0, 0); // Main Tools Section QLabel *mainToolsLabel = new QLabel("Tools"); mainToolsLabel->setStyleSheet( "font-weight: bold; font-size: 14px; margin-left: 10px"); contentLayout->addWidget(mainToolsLabel); QWidget *mainToolsWidget = new QWidget(); m_gridLayout = new QGridLayout(mainToolsWidget); m_gridLayout->setSpacing(10); QList mainToolWidgets; mainToolWidgets.append( {iDescriptorTool::Airplayer, "Cast your device screen ", false, ""}); mainToolWidgets.append({iDescriptorTool::VirtualLocation, "Simulate GPS location on your device", true, ""}); mainToolWidgets.append({iDescriptorTool::LiveScreen, "View device screen in real-time", true, ""}); mainToolWidgets.append({iDescriptorTool::QueryMobileGestalt, "Query device hardware information", true, ""}); mainToolWidgets.append({iDescriptorTool::DeveloperDiskImages, "Manage developer disk images", false, ""}); mainToolWidgets.append( {iDescriptorTool::WirelessGalleryImport, "Import photos wirelessly to your iDevice (requires Shortcuts app)", false, ""}); #ifndef __APPLE__ mainToolWidgets.append({iDescriptorTool::iFuse, "Mount your iPhone's filesystem on your PC", true, ""}); #endif mainToolWidgets.append({iDescriptorTool::CableInfoWidget, "View detailed cable and connection info", true, ""}); mainToolWidgets.append({iDescriptorTool::NetworkDevices, "Discover and monitor devices on your network", false, ""}); for (int i = 0; i < mainToolWidgets.size(); ++i) { const auto &tool = mainToolWidgets[i]; ClickableWidget *toolbox = createToolbox(tool.tool, tool.description, tool.requiresDevice); int row = i / 3; int col = i % 3; m_gridLayout->addWidget(toolbox, row, col); } contentLayout->addWidget(mainToolsWidget); // More Tools Section QLabel *moreToolsLabel = new QLabel("More Tools"); moreToolsLabel->setStyleSheet( "font-weight: bold; font-size: 14px; margin-left: 10px"); contentLayout->addWidget(moreToolsLabel); QWidget *moreToolsWidget = new QWidget(); QGridLayout *moreGridLayout = new QGridLayout(moreToolsWidget); moreGridLayout->setSpacing(10); QList moreToolWidgets; moreToolWidgets.append( {iDescriptorTool::MountDevImage, "Mount a compatible device image with a single click", true, ""}); moreToolWidgets.append( {iDescriptorTool::Restart, "Restart device services", true, ""}); moreToolWidgets.append( {iDescriptorTool::Shutdown, "Shut down the device", true, ""}); moreToolWidgets.append({iDescriptorTool::RecoveryMode, "Enter device recovery mode", true, ""}); for (int i = 0; i < moreToolWidgets.size(); ++i) { const auto &tool = moreToolWidgets[i]; ClickableWidget *toolbox = createToolbox(tool.tool, tool.description, tool.requiresDevice); int row = i / 3; int col = i % 3; moreGridLayout->addWidget(toolbox, row, col); } contentLayout->addWidget(moreToolsWidget); contentLayout->addStretch(); m_scrollArea->setWidget(m_contentWidget); mainLayout->addWidget(m_scrollArea); connect(m_deviceCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ToolboxWidget::onDeviceSelectionChanged); } ClickableWidget *ToolboxWidget::createToolbox(iDescriptorTool tool, const QString &description, bool requiresDevice) { ClickableWidget *b = new ClickableWidget(); b->setStyleSheet("padding: 5px; border: none; outline: none;"); QVBoxLayout *layout = new QVBoxLayout(b); ZIconLabel *icon = new ZIconLabel(QIcon(), nullptr, 1.5, this); QString title; switch (tool) { case iDescriptorTool::Airplayer: title = "Airplayer"; icon->setIcon( QIcon(":/resources/icons/MaterialSymbolsLightAirplayOutline.png")); break; case iDescriptorTool::LiveScreen: title = "Live Screen"; icon->setIcon(QIcon(":/resources/icons/PepiconsPrintCellphoneEye.png")); break; case iDescriptorTool::MountDevImage: title = "Mount Dev Image"; icon->setIcon(QIcon(":/resources/icons/MdiDisk.png")); break; case iDescriptorTool::VirtualLocation: title = "Virtual Location"; icon->setIcon( QIcon(":/resources/icons/MaterialSymbolsLocationOnOutline.png")); break; case iDescriptorTool::Restart: title = "Restart"; icon->setIcon(QIcon(":/resources/icons/IcTwotoneRestartAlt.png")); break; case iDescriptorTool::Shutdown: title = "Shutdown"; icon->setIcon(QIcon(":/resources/icons/IcOutlinePowerSettingsNew.png")); break; case iDescriptorTool::RecoveryMode: title = "Recovery Mode"; icon->setIcon(QIcon(":/resources/icons/HugeiconsWrench01.png")); break; case iDescriptorTool::QueryMobileGestalt: title = "Query Mobile Gestalt"; icon->setIcon( QIcon(":/resources/icons/" "StreamlineProgrammingBrowserSearchSearchWindowGlassAppCod" "eProgrammingQueryFindMagnifyingApps.png")); break; case iDescriptorTool::DeveloperDiskImages: title = "Dev Disk Images"; icon->setIcon(QIcon(":/resources/icons/TablerDatabaseExport.png")); break; case iDescriptorTool::WirelessGalleryImport: title = "Wireless Gallery Import"; icon->setIcon( QIcon(":/resources/icons/MaterialSymbolsAndroidWifi3BarPlus.png")); break; case iDescriptorTool::iFuse: title = "iFuse Mount"; icon->setIcon(QIcon(":/resources/icons/fuse.png")); icon->setIconThemable(false); break; case iDescriptorTool::CableInfoWidget: title = "Cable Info"; icon->setIcon( QIcon(":/resources/icons/MaterialSymbolsLightCableRounded.png")); break; case iDescriptorTool::NetworkDevices: title = "Network Devices"; icon->setIcon(QIcon( ":/resources/icons/StreamlineUltimateMultipleUsersNetwork.png")); break; default: title = "Unknown Tool"; break; } // Title QLabel *titleLabel = new QLabel(title); titleLabel->setAlignment(Qt::AlignCenter); // Description QLabel *descLabel = new QLabel(description); descLabel->setWordWrap(true); descLabel->setAlignment(Qt::AlignCenter); descLabel->setStyleSheet("color: #666; font-size: 12px;"); icon->setIconSizeMultiplier(1.90); layout->addWidget(icon, 0, Qt::AlignCenter); layout->addWidget(titleLabel); layout->addWidget(descLabel); b->setCursor(Qt::PointingHandCursor); m_toolboxes.append(b); b->setProperty("requiresDevice", requiresDevice); connect(b, &ClickableWidget::clicked, [this, tool, requiresDevice]() { onToolboxClicked(tool, requiresDevice); }); return b; } void ToolboxWidget::updateDeviceList() { m_deviceCombo->blockSignals(true); m_deviceCombo->clear(); QList devices = AppContext::sharedInstance()->getAllDevices(); if (devices.isEmpty()) { m_deviceCombo->addItem("No device connected"); m_deviceCombo->setEnabled(false); m_uuid.clear(); } else { m_deviceCombo->setEnabled(true); for (iDescriptorDevice *device : devices) { QString shortUdid = QString::fromStdString(device->udid).left(8) + "..."; m_deviceCombo->addItem( QString::fromStdString(device->deviceInfo.productType) + " / " + shortUdid, QString::fromStdString(device->udid)); } } onCurrentDeviceChanged( AppContext::sharedInstance()->getCurrentDeviceSelection()); m_deviceCombo->blockSignals(false); } void ToolboxWidget::updateToolboxStates() { bool hasDevice = !AppContext::sharedInstance()->getAllDevices().isEmpty(); for (int i = 0; i < m_toolboxes.size(); ++i) { QWidget *toolbox = m_toolboxes[i]; bool requiresDevice = toolbox->property("requiresDevice").toBool(); bool enabled = !requiresDevice || hasDevice; toolbox->setEnabled(enabled); if (enabled) { toolbox->setStyleSheet("#toolboxFrame { " "border-radius: 5px; padding: 5px; }"); } else { toolbox->setStyleSheet("#toolboxFrame { border-radius: 5px; " "padding: 5px;" "opacity: 0.45; }"); } } } void ToolboxWidget::updateUI() { updateDeviceList(); updateToolboxStates(); } void ToolboxWidget::onDeviceSelectionChanged() { QString selectedUdid = m_deviceCombo->currentData().toString(); if (selectedUdid.isEmpty()) { m_uuid.clear(); return; } if (AppContext::sharedInstance()->getDevice(selectedUdid.toStdString()) == nullptr) { QMessageBox::warning(this, "Device Not Found", "The selected device is no longer connected."); m_uuid.clear(); // Clear stale UUID updateDeviceList(); return; } m_uuid = selectedUdid.toStdString(); // Update the selected device in main menu AppContext::sharedInstance()->setCurrentDeviceSelection( DeviceSelection(selectedUdid.toStdString())); } void ToolboxWidget::onCurrentDeviceChanged(const DeviceSelection &selection) { if (selection.type == DeviceSelection::Normal) { int index = m_deviceCombo->findData(QString::fromStdString(selection.udid)); if (index != -1) { // Block signals to prevent recursive calls when we update the UI m_deviceCombo->blockSignals(true); m_deviceCombo->setCurrentIndex(index); m_deviceCombo->blockSignals(false); m_uuid = selection.udid; } } } void ToolboxWidget::onToolboxClicked(iDescriptorTool tool, bool requiresDevice) { iDescriptorDevice *device = AppContext::sharedInstance()->getDevice(m_uuid); if (!device && requiresDevice) { QMessageBox::warning( this, "Device Disconnected ?", "Device just disconnected, please select a device."); return; } switch (tool) { case iDescriptorTool::Airplayer: { if (!m_airplayWindow) { m_airplayWindow = new AirPlayWindow(); connect(m_airplayWindow, &QObject::destroyed, this, [this]() { m_airplayWindow = nullptr; }); m_airplayWindow->setAttribute(Qt::WA_DeleteOnClose); m_airplayWindow->setWindowFlag(Qt::Window); m_airplayWindow->resize(400, 300); m_airplayWindow->show(); } else { m_airplayWindow->raise(); m_airplayWindow->activateWindow(); } } break; case iDescriptorTool::LiveScreen: { LiveScreenWidget *liveScreen = new LiveScreenWidget(device); liveScreen->setAttribute(Qt::WA_DeleteOnClose); liveScreen->show(); } break; case iDescriptorTool::RecoveryMode: { _enterRecoveryMode(device); } break; case iDescriptorTool::MountDevImage: { DevDiskImageHelper *devDiskImageHelper = new DevDiskImageHelper(device, this); connect(devDiskImageHelper, &DevDiskImageHelper::mountingCompleted, this, [this, devDiskImageHelper](bool success) { devDiskImageHelper->deleteLater(); if (success) { QMessageBox::information( this, "Success", "Developer image mounted successfully."); } else { QMessageBox::warning( this, "Failure", "Failed to mount developer image."); } }); devDiskImageHelper->start(); } break; case iDescriptorTool::VirtualLocation: { // Handle virtual location functionality VirtualLocation *virtualLocation = new VirtualLocation(device); virtualLocation->setAttribute(Qt::WA_DeleteOnClose); virtualLocation->setWindowFlag(Qt::Window); virtualLocation->resize(800, 600); virtualLocation->show(); } break; case iDescriptorTool::Restart: { restartDevice(device); } break; case iDescriptorTool::Shutdown: { shutdownDevice(device); } break; case iDescriptorTool::QueryMobileGestalt: { // Handle querying MobileGestalt QueryMobileGestaltWidget *queryMobileGestaltWidget = new QueryMobileGestaltWidget(device); queryMobileGestaltWidget->setAttribute(Qt::WA_DeleteOnClose); queryMobileGestaltWidget->setWindowFlag(Qt::Window); queryMobileGestaltWidget->resize(800, 600); queryMobileGestaltWidget->show(); } break; case iDescriptorTool::DeveloperDiskImages: { if (!m_devDiskImagesWidget) { m_devDiskImagesWidget = new DevDiskImagesWidget(device); m_devDiskImagesWidget->setAttribute(Qt::WA_DeleteOnClose); m_devDiskImagesWidget->setWindowFlag(Qt::Window); m_devDiskImagesWidget->resize(800, 600); connect(m_devDiskImagesWidget, &QObject::destroyed, this, [this]() { m_devDiskImagesWidget = nullptr; }); m_devDiskImagesWidget->show(); } else { m_devDiskImagesWidget->raise(); m_devDiskImagesWidget->activateWindow(); } } break; case iDescriptorTool::WirelessGalleryImport: { if (!m_wirelessGalleryImportWidget) { m_wirelessGalleryImportWidget = new WirelessGalleryImportWidget(); connect(m_wirelessGalleryImportWidget, &QObject::destroyed, this, [this]() { m_wirelessGalleryImportWidget = nullptr; }); m_wirelessGalleryImportWidget->setAttribute(Qt::WA_DeleteOnClose); m_wirelessGalleryImportWidget->setWindowFlag(Qt::Window); // m_wirelessGalleryImportWidget->resize(800, 600); m_wirelessGalleryImportWidget->show(); } else { m_wirelessGalleryImportWidget->raise(); m_wirelessGalleryImportWidget->activateWindow(); } } break; #ifndef __APPLE__ case iDescriptorTool::iFuse: { if (!m_ifuseWidget) { m_ifuseWidget = new iFuseWidget(device); qDebug() << "Created iFuseWidget" << device->deviceInfo.productType.c_str(); m_ifuseWidget->setAttribute(Qt::WA_DeleteOnClose); connect(m_ifuseWidget, &QObject::destroyed, this, [this]() { m_ifuseWidget = nullptr; }); m_ifuseWidget->setWindowFlag(Qt::Window); m_ifuseWidget->resize(600, 400); m_ifuseWidget->show(); } else { m_ifuseWidget->raise(); m_ifuseWidget->activateWindow(); } } break; #endif case iDescriptorTool::CableInfoWidget: { CableInfoWidget *cableInfoWidget = new CableInfoWidget(device); cableInfoWidget->setAttribute(Qt::WA_DeleteOnClose); cableInfoWidget->setWindowFlag(Qt::Window); cableInfoWidget->resize(600, 400); cableInfoWidget->show(); } break; case iDescriptorTool::NetworkDevices: { if (!m_networkDevicesWidget) { m_networkDevicesWidget = new NetworkDevicesWidget(); m_networkDevicesWidget->setAttribute(Qt::WA_DeleteOnClose); m_networkDevicesWidget->setWindowFlag(Qt::Window); m_networkDevicesWidget->resize(500, 600); connect(m_networkDevicesWidget, &QObject::destroyed, this, [this]() { m_networkDevicesWidget = nullptr; }); m_networkDevicesWidget->show(); } else { m_networkDevicesWidget->raise(); m_networkDevicesWidget->activateWindow(); } } break; default: qDebug() << "Clicked on unimplemented tool"; break; } } void ToolboxWidget::restartDevice(iDescriptorDevice *device) { QMessageBox msgBox; msgBox.setWindowTitle("Restart Device"); msgBox.setText("Are you sure you want to restart the device?"); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); int ret = msgBox.exec(); if (ret != QMessageBox::Yes) return; if (!device || device->udid.empty()) { return; } if (!(restart(device->udid))) warn("Failed to restart device"); else { warn("Device will restart once unplugged", "Success"); qDebug() << "Restarting device"; } } void ToolboxWidget::shutdownDevice(iDescriptorDevice *device) { QMessageBox msgBox; msgBox.setWindowTitle("Shutdown Device"); msgBox.setText("Are you sure you want to shutdown the device?"); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); int ret = msgBox.exec(); if (ret != QMessageBox::Yes) return; if (!device || device->udid.empty()) { return; } if (!(shutdown(device->device))) // TODO: warn is a safe wrapper for QMessageBox but do we actually need // it ? warn("Failed to shutdown device"); else { warn("Device will shutdown once unplugged", "Success"); qDebug() << "Shutting down device"; } } void ToolboxWidget::_enterRecoveryMode(iDescriptorDevice *device) { QMessageBox msgBox; msgBox.setWindowTitle("Enter Recovery Mode"); msgBox.setText("Are you sure you want to enter recovery mode?"); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); int ret = msgBox.exec(); if (ret != QMessageBox::Yes) return; if (!device || device->udid.empty()) { return; } bool success = enterRecoveryMode(device); QMessageBox _msgBox; _msgBox.setWindowTitle("Recovery Mode"); if (success) { _msgBox.setText("Successfully entered recovery mode."); } else { _msgBox.setText("Failed to enter recovery mode."); } _msgBox.exec(); } void ToolboxWidget::restartAirPlayWindow() { if (!m_airplayWindow) { onToolboxClicked(iDescriptorTool::Airplayer, false); return; } connect( m_airplayWindow, &QObject::destroyed, this, [this]() { // give some time for cleanup QTimer::singleShot(100, this, [this]() { onToolboxClicked(iDescriptorTool::Airplayer, false); }); }, Qt::SingleShotConnection); m_airplayWindow->close(); }