From b4379505c7c4bb94f72764d0148fbc6956357684 Mon Sep 17 00:00:00 2001 From: uncor3 Date: Sat, 27 Sep 2025 20:27:22 +0000 Subject: [PATCH] WIP: Implement Installed Apps Widget and AFC2 file explorer - Added InstalledAppsWidget for displaying installed applications on the device. - Created AppTabWidget for individual app representation with icon and details. - Integrated fetching of app icons from Apple and displaying them in the widget. - Implemented search functionality to filter installed apps. - Added functionality to manage favorite places in SettingsManager. - Introduced methods to save, remove, and retrieve favorite places with proper cleanup of invalid entries. - Enhanced UI with responsive design and improved user interaction. - Added support for AFC2 and improved file exploration capabilities. --- src/appcontext.cpp | 1 + src/appswidget.cpp | 58 -- .../helpers/fetch_app_icon_from_apple.cpp | 61 ++ src/core/services/afc2_client_new.cpp | 35 + src/core/services/init_device.cpp | 18 +- src/deviceinfowidget.cpp | 21 +- src/devicelistener.h | 33 + src/devicemenuwidget.cpp | 17 +- src/fileexplorerwidget.cpp | 273 +++++- src/fileexplorerwidget.h | 26 + src/iDescriptor-ui.h | 21 + src/iDescriptor.h | 12 +- src/installedappswidget.cpp | 840 ++++++++++++++++++ src/installedappswidget.h | 103 +++ src/settingsmanager.cpp | 127 ++- src/settingsmanager.h | 26 +- 16 files changed, 1538 insertions(+), 134 deletions(-) create mode 100644 src/core/helpers/fetch_app_icon_from_apple.cpp create mode 100644 src/core/services/afc2_client_new.cpp create mode 100644 src/devicelistener.h create mode 100644 src/iDescriptor-ui.h create mode 100644 src/installedappswidget.cpp create mode 100644 src/installedappswidget.h diff --git a/src/appcontext.cpp b/src/appcontext.cpp index 35a3f40..ac2a509 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -111,6 +111,7 @@ void AppContext::addDevice(QString udid, idevice_connection_type conn_type, .device = initResult.device, .deviceInfo = initResult.deviceInfo, .afcClient = initResult.afcClient, + .afc2Client = initResult.afc2Client, }; m_devices[device->udid] = device; if (addType == AddType::Regular) diff --git a/src/appswidget.cpp b/src/appswidget.cpp index e0c4cc6..df7f8a5 100644 --- a/src/appswidget.cpp +++ b/src/appswidget.cpp @@ -28,8 +28,6 @@ #include #include #include -#include -#include #include #include #include @@ -45,62 +43,6 @@ #include #include -// Callback: void(QPixmap) -// TODO : move to utils -void fetchAppIconFromApple(const QString &bundleId, - std::function callback, - QObject *context) -{ - QNetworkAccessManager *manager = new QNetworkAccessManager(context); - QString url = - QString("https://itunes.apple.com/lookup?bundleId=%1").arg(bundleId); - - QNetworkReply *reply = manager->get(QNetworkRequest(QUrl(url))); - QObject::connect( - reply, &QNetworkReply::finished, context, - [reply, callback, manager, context]() { - QByteArray data = reply->readAll(); - reply->deleteLater(); - - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); - if (parseError.error != QJsonParseError::NoError) { - callback(QPixmap()); - manager->deleteLater(); - return; - } - - QJsonObject obj = doc.object(); - QJsonArray results = obj.value("results").toArray(); - if (results.isEmpty()) { - callback(QPixmap()); - manager->deleteLater(); - return; - } - - QJsonObject appInfo = results.at(0).toObject(); - QString iconUrl = appInfo.value("artworkUrl100").toString(); - if (iconUrl.isEmpty()) { - callback(QPixmap()); - manager->deleteLater(); - return; - } - - // Fetch the icon image - QNetworkReply *iconReply = - manager->get(QNetworkRequest(QUrl(iconUrl))); - QObject::connect(iconReply, &QNetworkReply::finished, context, - [iconReply, callback, manager]() { - QByteArray iconData = iconReply->readAll(); - iconReply->deleteLater(); - QPixmap pixmap; - pixmap.loadFromData(iconData); - callback(pixmap); - manager->deleteLater(); - }); - }); -} - AppsWidget::AppsWidget(QWidget *parent) : QWidget(parent), m_isLoggedIn(false) { // m_searchProcess = new QProcess(this); diff --git a/src/core/helpers/fetch_app_icon_from_apple.cpp b/src/core/helpers/fetch_app_icon_from_apple.cpp new file mode 100644 index 0000000..a7e38a5 --- /dev/null +++ b/src/core/helpers/fetch_app_icon_from_apple.cpp @@ -0,0 +1,61 @@ +#include +#include +#include +#include +#include +#include +#include + +void fetchAppIconFromApple(const QString &bundleId, + std::function callback, + QObject *context) +{ + QNetworkAccessManager *manager = new QNetworkAccessManager(context); + QString url = + QString("https://itunes.apple.com/lookup?bundleId=%1").arg(bundleId); + + QNetworkReply *reply = manager->get(QNetworkRequest(QUrl(url))); + QObject::connect( + reply, &QNetworkReply::finished, context, + [reply, callback, manager, context]() { + QByteArray data = reply->readAll(); + reply->deleteLater(); + + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); + if (parseError.error != QJsonParseError::NoError) { + callback(QPixmap()); + manager->deleteLater(); + return; + } + + QJsonObject obj = doc.object(); + QJsonArray results = obj.value("results").toArray(); + if (results.isEmpty()) { + callback(QPixmap()); + manager->deleteLater(); + return; + } + + QJsonObject appInfo = results.at(0).toObject(); + QString iconUrl = appInfo.value("artworkUrl100").toString(); + if (iconUrl.isEmpty()) { + callback(QPixmap()); + manager->deleteLater(); + return; + } + + // Fetch the icon image + QNetworkReply *iconReply = + manager->get(QNetworkRequest(QUrl(iconUrl))); + QObject::connect(iconReply, &QNetworkReply::finished, context, + [iconReply, callback, manager]() { + QByteArray iconData = iconReply->readAll(); + iconReply->deleteLater(); + QPixmap pixmap; + pixmap.loadFromData(iconData); + callback(pixmap); + manager->deleteLater(); + }); + }); +} diff --git a/src/core/services/afc2_client_new.cpp b/src/core/services/afc2_client_new.cpp new file mode 100644 index 0000000..99dcfa7 --- /dev/null +++ b/src/core/services/afc2_client_new.cpp @@ -0,0 +1,35 @@ +#include "../../iDescriptor.h" +#include +#include +#include +#include + +afc_error_t afc2_client_new(idevice_t device, afc_client_t *afc) +{ + + lockdownd_service_descriptor_t service = NULL; + // TODO: should free service ? + lockdownd_client_t client = NULL; + + if (lockdownd_client_new_with_handshake(device, &client, APP_LABEL) != + LOCKDOWN_E_SUCCESS) { + qDebug() << "Could not connect to lockdownd"; + return AFC_E_UNKNOWN_ERROR; + } + if (lockdownd_start_service(client, AFC2_SERVICE_NAME, &service) != + LOCKDOWN_E_SUCCESS) { + qDebug() << "Could not start AFC service"; + lockdownd_client_free(client); + return AFC_E_UNKNOWN_ERROR; + } + + return afc_client_new(device, service, afc); + + // char **dirs = NULL; + // if (afc_read_directory(afc, argv[1], &dirs) == AFC_E_SUCCESS) { + // for (int i = 0; dirs[i]; i++) { + // printf("Entry: %s\n", dirs[i]); + // } + // // free(dirs); + // } +} \ No newline at end of file diff --git a/src/core/services/init_device.cpp b/src/core/services/init_device.cpp index 00070b6..e1cb872 100644 --- a/src/core/services/init_device.cpp +++ b/src/core/services/init_device.cpp @@ -209,7 +209,6 @@ DeviceInfo fullDeviceInfo(const pugi::xml_document &doc, /*BatteryInfo*/ plist_t diagnostics = nullptr; get_battery_info(rawProductType, result.device, d.is_iPhone, diagnostics); - plist_print(diagnostics); if (!diagnostics) { qDebug() << "Failed to get diagnostics plist."; @@ -266,7 +265,7 @@ DeviceInfo fullDeviceInfo(const pugi::xml_document &doc, } } -// TODO: need to handle errors and free resources properly +// TODO: IDescriptorInitDeviceResult IDescriptorInitDeviceResult init_idescriptor_device(const char *udid) { // TODO:on a broken usb cable this can hang for a long time @@ -281,6 +280,7 @@ IDescriptorInitDeviceResult init_idescriptor_device(const char *udid) lockdownd_error_t ldret = LOCKDOWN_E_UNKNOWN_ERROR; lockdownd_service_descriptor_t lockdownService = nullptr; afc_client_t afcClient = nullptr; + afc_client_t afc2Client = nullptr; try { idevice_error_t ret = idevice_new_with_options(&result.device, udid, IDEVICE_LOOKUP_USBMUX); @@ -323,6 +323,20 @@ IDescriptorInitDeviceResult init_idescriptor_device(const char *udid) return result; } + try { + afc_error_t err = AFC_E_UNKNOWN_ERROR; + if ((err = afc2_client_new(result.device, &afc2Client)) != + AFC_E_SUCCESS) { + qDebug() << "AFC2 client not available." << "Error:" << err; + } else { + result.afc2Client = afc2Client; + qDebug() << "AFC2 client created successfully."; + } + } catch (const std::exception &e) { + /* Fine! This only works on Jailbroken and AFC2 tweak installed + * devices */ + } + pugi::xml_document infoXml; get_device_info_xml(udid, 0, 0, infoXml, client, result.device); diff --git a/src/deviceinfowidget.cpp b/src/deviceinfowidget.cpp index d1ae27c..97c7efe 100644 --- a/src/deviceinfowidget.cpp +++ b/src/deviceinfowidget.cpp @@ -2,6 +2,7 @@ #include "batterywidget.h" #include "diskusagewidget.h" #include "fileexplorerwidget.h" +#include "iDescriptor-ui.h" #include "iDescriptor.h" #include #include @@ -20,26 +21,6 @@ #include #include -// A custom QGraphicsView that keeps the content fitted with aspect ratio on -// resize -class ResponsiveGraphicsView : public QGraphicsView -{ -public: - ResponsiveGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr) - : QGraphicsView(scene, parent) - { - } - -protected: - void resizeEvent(QResizeEvent *event) override - { - if (scene() && !scene()->items().isEmpty()) { - fitInView(scene()->itemsBoundingRect(), Qt::KeepAspectRatio); - } - QGraphicsView::resizeEvent(event); - } -}; - DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) : QWidget(parent), m_device(device) { diff --git a/src/devicelistener.h b/src/devicelistener.h new file mode 100644 index 0000000..ffc4640 --- /dev/null +++ b/src/devicelistener.h @@ -0,0 +1,33 @@ +// #ifndef DEVICELISTENER_H +// #define DEVICELISTENER_H +// #include +// #include +// #include +// #include +// #include + +// class DeviceListener : public QObject +// { +// Q_OBJECT + +// public: +// DeviceListener(); +// ~DeviceListener(); + +// private slots: +// void clientConnected(); +// void clientDisconnected(); + +// private: +// void setupVirtualKeyboard(); + +// QLowEnergyAdvertisingData m_advertisingData; +// QLowEnergyServiceData m_hidServiceData; + +// QLowEnergyController *m_leController = nullptr; +// QLowEnergyService *m_service = nullptr; +// QLowEnergyCharacteristic +// m_inputReportChar; // Keep a reference to the characteristic +// }; + +// #endif // DEVICELISTENER_H diff --git a/src/devicemenuwidget.cpp b/src/devicemenuwidget.cpp index 89c492e..8bcc9c6 100644 --- a/src/devicemenuwidget.cpp +++ b/src/devicemenuwidget.cpp @@ -3,6 +3,7 @@ #include "fileexplorerwidget.h" #include "gallerywidget.h" #include "iDescriptor.h" +#include "installedappswidget.h" #include #include #include @@ -17,19 +18,19 @@ DeviceMenuWidget::DeviceMenuWidget(iDescriptorDevice *device, QWidget *parent) QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget); mainLayout->addWidget(tabWidget); - tabWidget->addTab(new DeviceInfoWidget(device, this), ""); - - // FIXME:race condition with lockdownd_client_new_with_handshake FileExplorerWidget *explorer = new FileExplorerWidget(device, this); explorer->setMinimumHeight(300); - tabWidget->addTab(explorer, ""); - GalleryWidget *gallery = new GalleryWidget(device, this); - unsigned int galleryIndex = tabWidget->addTab(gallery, ""); gallery->setMinimumHeight(300); setLayout(mainLayout); + tabWidget->addTab(new DeviceInfoWidget(device, this), ""); + tabWidget->addTab(new InstalledAppsWidget(device, this), ""); + unsigned int galleryIndex = tabWidget->addTab(gallery, ""); + tabWidget->addTab(explorer, ""); + + // TODO : one time ? connect(tabWidget, &QTabWidget::currentChanged, this, [this, galleryIndex, gallery](int index) { if (index == galleryIndex) { @@ -43,10 +44,12 @@ void DeviceMenuWidget::switchToTab(const QString &tabName) { if (tabName == "Info") { tabWidget->setCurrentIndex(0); - } else if (tabName == "Files") { + } else if (tabName == "Apps") { tabWidget->setCurrentIndex(1); } else if (tabName == "Gallery") { tabWidget->setCurrentIndex(2); + } else if (tabName == "Files") { + tabWidget->setCurrentIndex(3); } else { qDebug() << "Tab not found:" << tabName; } diff --git a/src/fileexplorerwidget.cpp b/src/fileexplorerwidget.cpp index 625e8e8..0689237 100644 --- a/src/fileexplorerwidget.cpp +++ b/src/fileexplorerwidget.cpp @@ -1,68 +1,58 @@ #include "fileexplorerwidget.h" #include "iDescriptor.h" #include "mediapreviewdialog.h" +#include "settingsmanager.h" #include #include #include #include +#include #include +#include #include #include #include #include +#include +#include #include #include #include FileExplorerWidget::FileExplorerWidget(iDescriptorDevice *device, QWidget *parent) - : QWidget(parent), device(device) + : QWidget(parent), device(device), usingAFC2(false) { - QVBoxLayout *mainLayout = new QVBoxLayout(this); + // Initialize current AFC client to default + currentAfcClient = device->afcClient; + qDebug() << "AFC2 available:" << (device->afc2Client != nullptr); + // Create main splitter + mainSplitter = new QSplitter(Qt::Horizontal, this); - // --- New: Export/Import buttons layout --- - QHBoxLayout *exportLayout = new QHBoxLayout(); - exportBtn = new QPushButton("Export"); - exportDeleteBtn = new QPushButton("Export & Delete"); - importBtn = new QPushButton("Import"); // NEW - exportLayout->addWidget(exportBtn); - exportLayout->addWidget(exportDeleteBtn); - exportLayout->addWidget(importBtn); // NEW - exportLayout->addStretch(); - mainLayout->addLayout(exportLayout); + // Setup sidebar + setupSidebar(); - // --- Navigation layout (Back + Breadcrumb) --- - QHBoxLayout *navLayout = new QHBoxLayout(); - backBtn = new QPushButton("Back"); - breadcrumbLayout = new QHBoxLayout(); - breadcrumbLayout->setSpacing(0); - navLayout->addWidget(backBtn); - navLayout->addLayout(breadcrumbLayout); - navLayout->addStretch(); - mainLayout->addLayout(navLayout); + // Setup file explorer + setupFileExplorer(); - fileList = new QListWidget(); - fileList->setSelectionMode( - QAbstractItemView::ExtendedSelection); // Enable multi-select - mainLayout->addWidget(fileList); - - connect(backBtn, &QPushButton::clicked, this, &FileExplorerWidget::goBack); - connect(fileList, &QListWidget::itemDoubleClicked, this, - &FileExplorerWidget::onItemDoubleClicked); - - // --- New: Export/Import buttons connections --- - connect(exportBtn, &QPushButton::clicked, this, - &FileExplorerWidget::onExportClicked); - connect(exportDeleteBtn, &QPushButton::clicked, this, - &FileExplorerWidget::onExportDeleteClicked); - connect(importBtn, &QPushButton::clicked, this, - &FileExplorerWidget::onImportClicked); // NEW + // Add widgets to splitter + mainSplitter->addWidget(sidebarTree); + mainSplitter->addWidget(fileExplorerWidget); + mainSplitter->setSizes({400, 800}); + // Main layout + QHBoxLayout *mainLayout = new QHBoxLayout(this); + mainLayout->addWidget(mainSplitter); setLayout(mainLayout); + + // Initialize history.push("/"); loadPath("/"); setupContextMenu(); + connect(SettingsManager::sharedInstance(), + &SettingsManager::favoritePlacesChanged, this, + &FileExplorerWidget::refreshFavoritePlaces); } void FileExplorerWidget::goBack() @@ -162,7 +152,7 @@ void FileExplorerWidget::loadPath(const QString &path) updateBreadcrumb(path); MediaFileTree tree = - get_file_tree(device->afcClient, device->device, path.toStdString()); + get_file_tree(currentAfcClient, device->device, path.toStdString()); if (!tree.success) { fileList->addItem("Failed to load directory"); return; @@ -283,7 +273,7 @@ void FileExplorerWidget::exportSelectedFile(QListWidgetItem *item, // Export file using the validated connections int result = - export_file_to_path(device->afcClient, devicePath.toStdString().c_str(), + export_file_to_path(currentAfcClient, devicePath.toStdString().c_str(), savePath.toStdString().c_str()); qDebug() << "Export result:" << result; @@ -366,7 +356,7 @@ void FileExplorerWidget::onImportClicked() for (const QString &localPath : fileNames) { QFileInfo fi(localPath); QString devicePath = currPath + fi.fileName(); - int result = import_file_to_device(device->afcClient, + int result = import_file_to_device(currentAfcClient, devicePath.toStdString().c_str(), localPath.toStdString().c_str()); if (result == 0) @@ -416,3 +406,206 @@ int FileExplorerWidget::import_file_to_device(afc_client_t afc, in.close(); return 0; } + +// useAFC2 ,path, +typedef QPair SidebarItemData; + +void FileExplorerWidget::setupSidebar() +{ + sidebarTree = new QTreeWidget(); + sidebarTree->setHeaderLabel("Files"); + sidebarTree->setMinimumWidth(350); + sidebarTree->setMaximumWidth(400); + + // AFC Default section + afcDefaultItem = new QTreeWidgetItem(sidebarTree); + afcDefaultItem->setText(0, "Explorer"); + afcDefaultItem->setIcon(0, QIcon::fromTheme("folder")); + afcDefaultItem->setData(0, Qt::UserRole, + QVariant::fromValue(SidebarItemData(false, "/"))); + afcDefaultItem->setExpanded(true); + + // Add root folder under Default + QTreeWidgetItem *rootItem = new QTreeWidgetItem(afcDefaultItem); + rootItem->setText(0, "Default"); + rootItem->setIcon(0, QIcon::fromTheme("folder")); + rootItem->setData(0, Qt::UserRole, + QVariant::fromValue(SidebarItemData(false, "/"))); + rootItem->setData(0, Qt::UserRole + 1, QVariant::fromValue(false)); + + // AFC2 Jailbroken section + afcJailbrokenItem = new QTreeWidgetItem(afcDefaultItem); + afcJailbrokenItem->setText(0, "Jailbroken (AFC2)"); + afcJailbrokenItem->setIcon(0, QIcon::fromTheme("applications-system")); + afcJailbrokenItem->setData(0, Qt::UserRole, + QVariant::fromValue(SidebarItemData(true, "/"))); + afcJailbrokenItem->setExpanded(false); + + // Common Places section + commonPlacesItem = new QTreeWidgetItem(sidebarTree); + commonPlacesItem->setText(0, "Common Places"); + commonPlacesItem->setIcon(0, QIcon::fromTheme("places-bookmarks")); + commonPlacesItem->setData( + 0, Qt::UserRole, + QVariant::fromValue( + SidebarItemData(false, "../../../var/mobile/Library/Wallpapers"))); + commonPlacesItem->setExpanded(true); + + QTreeWidgetItem *wallpapersItem = new QTreeWidgetItem(commonPlacesItem); + wallpapersItem->setText(0, "Wallpapers"); + wallpapersItem->setIcon(0, QIcon::fromTheme("image-x-generic")); + wallpapersItem->setData( + 0, Qt::UserRole, + QVariant::fromValue( + SidebarItemData(false, "../../../var/mobile/Library/Wallpapers"))); + wallpapersItem->setData(0, Qt::UserRole + 1, + QVariant::fromValue(false)); // Default AFC + + // Favorite Places section + favoritePlacesItem = new QTreeWidgetItem(sidebarTree); + favoritePlacesItem->setText(0, "Favorite Places"); + favoritePlacesItem->setIcon(0, QIcon::fromTheme("user-bookmarks")); + favoritePlacesItem->setData( + // todo:implement + 0, Qt::UserRole, QVariant::fromValue(SidebarItemData(false, "/"))); + favoritePlacesItem->setExpanded(true); + + loadFavoritePlaces(); + + connect(sidebarTree, &QTreeWidget::itemClicked, this, + &FileExplorerWidget::onSidebarItemClicked); +} + +void FileExplorerWidget::setupFileExplorer() +{ + fileExplorerWidget = new QWidget(); + QVBoxLayout *explorerLayout = new QVBoxLayout(fileExplorerWidget); + + // Export/Import buttons layout + QHBoxLayout *exportLayout = new QHBoxLayout(); + exportBtn = new QPushButton("Export"); + exportDeleteBtn = new QPushButton("Export & Delete"); + importBtn = new QPushButton("Import"); + addToFavoritesBtn = new QPushButton("Add to Favorites"); + exportLayout->addWidget(exportBtn); + exportLayout->addWidget(exportDeleteBtn); + exportLayout->addWidget(importBtn); + exportLayout->addWidget(addToFavoritesBtn); + exportLayout->addStretch(); + explorerLayout->addLayout(exportLayout); + + // Navigation layout (Back + Breadcrumb) + QHBoxLayout *navLayout = new QHBoxLayout(); + backBtn = new QPushButton("Back"); + breadcrumbLayout = new QHBoxLayout(); + breadcrumbLayout->setSpacing(0); + navLayout->addWidget(backBtn); + navLayout->addLayout(breadcrumbLayout); + navLayout->addStretch(); + explorerLayout->addLayout(navLayout); + + // File list + fileList = new QListWidget(); + fileList->setSelectionMode(QAbstractItemView::ExtendedSelection); + explorerLayout->addWidget(fileList); + + // Connect buttons + connect(backBtn, &QPushButton::clicked, this, &FileExplorerWidget::goBack); + connect(fileList, &QListWidget::itemDoubleClicked, this, + &FileExplorerWidget::onItemDoubleClicked); + connect(exportBtn, &QPushButton::clicked, this, + &FileExplorerWidget::onExportClicked); + connect(exportDeleteBtn, &QPushButton::clicked, this, + &FileExplorerWidget::onExportDeleteClicked); + connect(importBtn, &QPushButton::clicked, this, + &FileExplorerWidget::onImportClicked); + connect(addToFavoritesBtn, &QPushButton::clicked, this, + &FileExplorerWidget::onAddToFavoritesClicked); +} + +void FileExplorerWidget::onSidebarItemClicked(QTreeWidgetItem *item, int column) +{ + Q_UNUSED(column) + + bool useAfc2 = item->data(0, Qt::UserRole).value().first; + QString path = item->data(0, Qt::UserRole).value().second; + + // if (itemType == "try_install_afc2") { + // onTryInstallAFC2Clicked(); + // return; + // } + + switchToAFC(useAfc2); + loadPath(path); +} + +void FileExplorerWidget::onAddToFavoritesClicked() +{ + QString currentPath = "/"; + if (!history.isEmpty()) + currentPath = history.top(); + + bool ok; + QString alias = QInputDialog::getText( + this, "Add to Favorites", + "Enter alias for this location:", QLineEdit::Normal, currentPath, &ok); + if (ok && !alias.isEmpty()) { + saveFavoritePlace(currentPath, alias); + refreshFavoritePlaces(); + } +} + +void FileExplorerWidget::onTryInstallAFC2Clicked() +{ + qDebug() << "Clicked on try to install AFC2"; +} + +void FileExplorerWidget::switchToAFC(bool useAFC2) +{ + if (useAFC2 && device->afc2Client) { + usingAFC2 = true; + currentAfcClient = device->afc2Client; + } else { + usingAFC2 = false; + currentAfcClient = device->afcClient; + } +} + +void FileExplorerWidget::loadFavoritePlaces() +{ + SettingsManager *settings = SettingsManager::sharedInstance(); + QList> favorites = settings->getFavoritePlaces(); + qDebug() << "Loading favorite places:" << favorites.size(); + for (const auto &favorite : favorites) { + QString path = favorite.first; + QString alias = favorite.second; + + qDebug() << "Favorite:" << alias << "->" << path; + QTreeWidgetItem *favoriteItem = new QTreeWidgetItem(favoritePlacesItem); + favoriteItem->setText(0, alias); + favoriteItem->setIcon(0, QIcon::fromTheme("folder-favorites")); + favoriteItem->setData( + 0, Qt::UserRole, QVariant::fromValue(SidebarItemData(false, path))); + favoriteItem->setData(0, Qt::UserRole + 1, + QVariant::fromValue(false)); // Default to AFC + } +} + +void FileExplorerWidget::saveFavoritePlace(const QString &path, + const QString &alias) +{ + qDebug() << "Saving favorite place:" << alias << "->" << path; + SettingsManager *settings = SettingsManager::sharedInstance(); + settings->saveFavoritePlace(path, alias); +} + +void FileExplorerWidget::refreshFavoritePlaces() +{ + // Clear existing favorite items + while (favoritePlacesItem->childCount() > 0) { + delete favoritePlacesItem->takeChild(0); + } + + // Reload favorite places + loadFavoritePlaces(); +} diff --git a/src/fileexplorerwidget.h b/src/fileexplorerwidget.h index 6f14177..c345af6 100644 --- a/src/fileexplorerwidget.h +++ b/src/fileexplorerwidget.h @@ -3,12 +3,15 @@ #include "iDescriptor.h" #include +#include #include #include #include #include +#include #include #include +#include #include #include #include @@ -31,19 +34,42 @@ private slots: void onExportClicked(); void onExportDeleteClicked(); void onImportClicked(); + void onSidebarItemClicked(QTreeWidgetItem *item, int column); + void onAddToFavoritesClicked(); + void onTryInstallAFC2Clicked(); private: + QSplitter *mainSplitter; + QTreeWidget *sidebarTree; + QWidget *fileExplorerWidget; QPushButton *backBtn; QPushButton *exportBtn; QPushButton *exportDeleteBtn; QPushButton *importBtn; + QPushButton *addToFavoritesBtn; QListWidget *fileList; QStack history; QHBoxLayout *breadcrumbLayout; iDescriptorDevice *device; + // Current AFC mode + bool usingAFC2; + afc_client_t currentAfcClient; + + // Tree items + QTreeWidgetItem *afcDefaultItem; + QTreeWidgetItem *afcJailbrokenItem; + QTreeWidgetItem *commonPlacesItem; + QTreeWidgetItem *favoritePlacesItem; + + void setupSidebar(); + void setupFileExplorer(); void loadPath(const QString &path); void updateBreadcrumb(const QString &path); + void loadFavoritePlaces(); + void saveFavoritePlace(const QString &path, const QString &alias); + void refreshFavoritePlaces(); + void switchToAFC(bool useAFC2); void setupContextMenu(); void exportSelectedFile(QListWidgetItem *item); diff --git a/src/iDescriptor-ui.h b/src/iDescriptor-ui.h new file mode 100644 index 0000000..025db7d --- /dev/null +++ b/src/iDescriptor-ui.h @@ -0,0 +1,21 @@ +#pragma once +#include +// A custom QGraphicsView that keeps the content fitted with aspect ratio on +// resize +class ResponsiveGraphicsView : public QGraphicsView +{ +public: + ResponsiveGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr) + : QGraphicsView(scene, parent) + { + } + +protected: + void resizeEvent(QResizeEvent *event) override + { + if (scene() && !scene()->items().isEmpty()) { + fitInView(scene()->itemsBoundingRect(), Qt::KeepAspectRatio); + } + QGraphicsView::resizeEvent(event); + } +}; \ No newline at end of file diff --git a/src/iDescriptor.h b/src/iDescriptor.h index b1d0723..f63cfe7 100644 --- a/src/iDescriptor.h +++ b/src/iDescriptor.h @@ -15,7 +15,7 @@ #define APP_LABEL "iDescriptor" #define APP_VERSION "0.0.1" #define APP_COPYRIGHT "© 2023 Uncore. All rights reserved." - +#define AFC2_SERVICE_NAME "com.apple.afc2" #define RECOVERY_CLIENT_CONNECTION_TRIES 3 #define APPLE_VENDOR_ID 0x05ac @@ -144,6 +144,7 @@ struct iDescriptorDevice { clients are not long lived, so do not assume this will be valid */ afc_client_t afcClient; + afc_client_t afc2Client; bool is_iPhone; }; @@ -153,6 +154,7 @@ struct IDescriptorInitDeviceResult { idevice_t device; DeviceInfo deviceInfo; afc_client_t afcClient; + afc_client_t afc2Client; }; // Device model identifier to marketing name mapping @@ -440,4 +442,10 @@ void get_battery_info(std::string productType, idevice_t idevice, bool is_iphone, plist_t &diagnostics); void parseOldDeviceBattery(PlistNavigator &ioreg, DeviceInfo &d); -void parseDeviceBattery(PlistNavigator &ioreg, DeviceInfo &d); \ No newline at end of file +void parseDeviceBattery(PlistNavigator &ioreg, DeviceInfo &d); + +void fetchAppIconFromApple(const QString &bundleId, + std::function callback, + QObject *context); + +afc_error_t afc2_client_new(idevice_t device, afc_client_t *afc); diff --git a/src/installedappswidget.cpp b/src/installedappswidget.cpp new file mode 100644 index 0000000..90bf3fe --- /dev/null +++ b/src/installedappswidget.cpp @@ -0,0 +1,840 @@ +#include "installedappswidget.h" +#include "iDescriptor.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// AppTabWidget Implementation +AppTabWidget::AppTabWidget(const QString &appName, const QString &bundleId, + const QString &version, QWidget *parent) + : QWidget(parent), m_appName(appName), m_bundleId(bundleId), + m_version(version) +{ + setFixedHeight(60); + setMinimumWidth(250); + setCursor(Qt::PointingHandCursor); + + // Load placeholder icon + m_icon = QApplication::style() + ->standardIcon(QStyle::SP_ComputerIcon) + .pixmap(32, 32); + + fetchAppIcon(); +} + +void AppTabWidget::fetchAppIcon() +{ + fetchAppIconFromApple( + m_bundleId, + [this](const QPixmap &pixmap) { + if (!pixmap.isNull()) { + QPixmap scaled = + pixmap.scaled(32, 32, Qt::KeepAspectRatioByExpanding, + Qt::SmoothTransformation); + QPixmap rounded(32, 32); + rounded.fill(Qt::transparent); + + QPainter painter(&rounded); + painter.setRenderHint(QPainter::Antialiasing); + QPainterPath path; + path.addRoundedRect(QRectF(0, 0, 32, 32), 8, 8); + painter.setClipPath(path); + painter.drawPixmap(0, 0, scaled); + painter.end(); + + m_icon = rounded; + update(); + } + }, + this); +} + +void AppTabWidget::setSelected(bool selected) +{ + m_selected = selected; + update(); +} + +void AppTabWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + // Background + QColor bgColor; + if (m_selected) { + bgColor = QColor(0, 122, 255); // #007AFF + } else if (m_hovered) { + bgColor = QColor(224, 224, 224); // #e0e0e0 + } else { + bgColor = QColor(245, 245, 245); // #f5f5f5 + } + + painter.fillRect(rect().adjusted(2, 2, -2, -2), bgColor); + + // Border + painter.setPen(QPen(QColor(204, 204, 204), 1)); // #ccc + painter.drawRoundedRect(rect().adjusted(2, 2, -2, -2), 4, 4); + + // Icon + QRect iconRect(10, 14, 32, 32); + painter.drawPixmap(iconRect, m_icon); + + // Text + QColor textColor = m_selected ? Qt::white : Qt::black; + painter.setPen(textColor); + + QFont font = painter.font(); + font.setWeight(QFont::Medium); + painter.setFont(font); + + QRect textRect(50, 10, width() - 60, 20); + QString displayText = m_appName; + if (displayText.length() > 20) { + displayText = displayText.left(17) + "..."; + } + painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, displayText); + + // Version + if (!m_version.isEmpty()) { + font.setPointSize(font.pointSize() - 1); + font.setWeight(QFont::Normal); + painter.setFont(font); + + QColor versionColor = + m_selected ? QColor(255, 255, 255, 180) : QColor(102, 102, 102); + painter.setPen(versionColor); + + QRect versionRect(50, 32, width() - 60, 16); + painter.drawText(versionRect, Qt::AlignLeft | Qt::AlignVCenter, + m_version); + } +} + +void AppTabWidget::mousePressEvent(QMouseEvent *event) +{ + Q_UNUSED(event) + emit clicked(); +} + +void AppTabWidget::enterEvent(QEnterEvent *event) +{ + Q_UNUSED(event) + m_hovered = true; + update(); +} + +void AppTabWidget::leaveEvent(QEvent *event) +{ + Q_UNUSED(event) + m_hovered = false; + update(); +} + +InstalledAppsWidget::InstalledAppsWidget(iDescriptorDevice *device, + QWidget *parent) + : QWidget(parent), m_device(device) +{ + m_watcher = new QFutureWatcher(this); + m_containerWatcher = new QFutureWatcher(this); + setupUI(); + + connect(m_watcher, &QFutureWatcher::finished, this, + &InstalledAppsWidget::onAppsDataReady); + connect(m_containerWatcher, &QFutureWatcher::finished, this, + &InstalledAppsWidget::onContainerDataReady); + + fetchInstalledApps(); +} + +void InstalledAppsWidget::setupUI() +{ + m_mainLayout = new QHBoxLayout(this); + m_mainLayout->setContentsMargins(0, 0, 0, 0); + m_mainLayout->setSpacing(0); + + // Left side - Custom tab area with scroll + QFrame *tabFrame = new QFrame(); + tabFrame->setMinimumWidth(350); + tabFrame->setMaximumWidth(400); + tabFrame->setStyleSheet("QFrame { border: 1px solid #ccc; }"); + + QVBoxLayout *tabFrameLayout = new QVBoxLayout(tabFrame); + tabFrameLayout->setContentsMargins(0, 0, 0, 0); + tabFrameLayout->setSpacing(0); + + // Search box at the top + QWidget *searchContainer = new QWidget(); + searchContainer->setFixedHeight(50); + QHBoxLayout *searchLayout = new QHBoxLayout(searchContainer); + searchLayout->setContentsMargins(10, 10, 10, 10); + + m_searchEdit = new QLineEdit(); + m_searchEdit->setPlaceholderText("Search apps..."); + m_searchEdit->setStyleSheet("QLineEdit { " + " border: 2px solid #e0e0e0; " + " border-radius: 6px; " + " padding: 8px 12px; " + " font-size: 14px; " + " color: #333; " + "} " + "QLineEdit:focus { " + " border: 2px solid #007AFF; " + " outline: none; " + "} " + "QLineEdit::placeholder { " + " color: #999; " + "}"); + + // Add search icon + QAction *searchAction = m_searchEdit->addAction( + this->style()->standardIcon(QStyle::SP_FileDialogContentsView), + QLineEdit::LeadingPosition); + searchAction->setToolTip("Search"); + + searchLayout->addWidget(m_searchEdit); + tabFrameLayout->addWidget(searchContainer); + + // Add a separator line + QFrame *separator = new QFrame(); + separator->setFrameShape(QFrame::HLine); + separator->setFrameShadow(QFrame::Sunken); + separator->setStyleSheet("QFrame { color: #e0e0e0; }"); + tabFrameLayout->addWidget(separator); + + m_tabScrollArea = new QScrollArea(); + m_tabScrollArea->setWidgetResizable(true); + m_tabScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_tabScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_tabScrollArea->setStyleSheet("QScrollArea { border: none; }"); + + m_tabContainer = new QWidget(); + // m_tabContainer->setStyleSheet(""); + m_tabLayout = new QVBoxLayout(m_tabContainer); + m_tabLayout->setContentsMargins(0, 0, 0, 0); + m_tabLayout->setSpacing(0); + m_tabLayout->addStretch(); // Push tabs to top + + m_tabScrollArea->setWidget(m_tabContainer); + tabFrameLayout->addWidget(m_tabScrollArea); + + // Right side - Content area + m_contentWidget = new QWidget(); + m_contentWidget->setStyleSheet("border: 1px solid #ccc;"); + + QVBoxLayout *contentLayout = new QVBoxLayout(m_contentWidget); + contentLayout->setContentsMargins(20, 20, 20, 20); + contentLayout->setSpacing(15); + + m_contentLabel = new QLabel("Select an app to view details"); + m_contentLabel->setAlignment(Qt::AlignCenter); + m_contentLabel->setStyleSheet("font-size: 16px; color: #666;"); + contentLayout->addWidget(m_contentLabel); + + // Container explorer area + QLabel *containerTitle = new QLabel("App Container:"); + containerTitle->setStyleSheet( + "font-size: 14px; font-weight: bold; color: #333;"); + containerTitle->setVisible(false); + contentLayout->addWidget(containerTitle); + + m_containerScrollArea = new QScrollArea(); + m_containerScrollArea->setWidgetResizable(true); + m_containerScrollArea->setMinimumHeight(200); + m_containerScrollArea->setStyleSheet( + "QScrollArea { border: 1px solid #ddd; border-radius: 4px; }"); + m_containerScrollArea->setVisible(false); + + m_containerWidget = new QWidget(); + m_containerLayout = new QVBoxLayout(m_containerWidget); + m_containerLayout->setContentsMargins(10, 10, 10, 10); + m_containerLayout->setSpacing(5); + + m_containerScrollArea->setWidget(m_containerWidget); + contentLayout->addWidget(m_containerScrollArea); + + // Progress bar for loading + m_progressBar = new QProgressBar(); + m_progressBar->setRange(0, 0); // Indeterminate progress + m_progressBar->setVisible(false); + contentLayout->addWidget(m_progressBar); + + m_mainLayout->addWidget(tabFrame); + m_mainLayout->addWidget(m_contentWidget, 1); // Give content area more space + + // Connect search functionality + connect(m_searchEdit, &QLineEdit::textChanged, this, + &InstalledAppsWidget::filterApps); + + showLoadingState(); +} + +void InstalledAppsWidget::showLoadingState() +{ + m_contentLabel->setText("Loading installed apps..."); + m_progressBar->setVisible(true); + + // Clear existing tabs + qDeleteAll(m_appTabs); + m_appTabs.clear(); + m_selectedTab = nullptr; +} + +void InstalledAppsWidget::showErrorState(const QString &error) +{ + m_contentLabel->setText(QString("Error loading apps: %1").arg(error)); + m_progressBar->setVisible(false); +} + +void InstalledAppsWidget::fetchInstalledApps() +{ + if (!m_device || !m_device->device) { + showErrorState("Invalid device"); + return; + } + + QFuture future = QtConcurrent::run([this]() -> QVariantMap { + QVariantMap result; + QVariantList apps; + + instproxy_client_t instproxy = nullptr; + lockdownd_client_t lockdownClient = nullptr; + lockdownd_service_descriptor_t lockdowndService = nullptr; + + try { + if (lockdownd_client_new_with_handshake( + m_device->device, &lockdownClient, APP_LABEL) != + LOCKDOWN_E_SUCCESS) { + result["error"] = "Could not connect to lockdown service"; + return result; + } + + if (lockdownd_start_service( + lockdownClient, "com.apple.mobile.installation_proxy", + &lockdowndService) != LOCKDOWN_E_SUCCESS) { + result["error"] = "Could not start installation proxy service"; + lockdownd_client_free(lockdownClient); + return result; + } + + if (instproxy_client_new(m_device->device, lockdowndService, + &instproxy) != INSTPROXY_E_SUCCESS) { + result["error"] = "Could not connect to installation proxy"; + lockdownd_service_descriptor_free(lockdowndService); + lockdownd_client_free(lockdownClient); + return result; + } + + lockdownd_service_descriptor_free(lockdowndService); + lockdowndService = nullptr; + + // Get both User and System apps + QStringList appTypes = {"User", "System"}; + + for (const QString &appType : appTypes) { + plist_t client_opts = plist_new_dict(); + plist_dict_set_item( + client_opts, "ApplicationType", + plist_new_string(appType.toUtf8().constData())); + + plist_t return_attrs = plist_new_array(); + plist_array_append_item(return_attrs, + plist_new_string("CFBundleIdentifier")); + plist_array_append_item( + return_attrs, plist_new_string("CFBundleDisplayName")); + plist_array_append_item( + return_attrs, + plist_new_string("CFBundleShortVersionString")); + plist_array_append_item(return_attrs, + plist_new_string("CFBundleVersion")); + + plist_dict_set_item(client_opts, "ReturnAttributes", + return_attrs); + + plist_t apps_plist = nullptr; + if (instproxy_browse(instproxy, client_opts, &apps_plist) == + INSTPROXY_E_SUCCESS && + apps_plist) { + if (plist_get_node_type(apps_plist) == PLIST_ARRAY) { + for (uint32_t i = 0; + i < plist_array_get_size(apps_plist); i++) { + plist_t app_info = + plist_array_get_item(apps_plist, i); + if (!app_info) + continue; + + QVariantMap appData; + + // Get bundle identifier + plist_t bundle_id = plist_dict_get_item( + app_info, "CFBundleIdentifier"); + if (bundle_id && plist_get_node_type(bundle_id) == + PLIST_STRING) { + char *bundle_id_str = nullptr; + plist_get_string_val(bundle_id, &bundle_id_str); + if (bundle_id_str) { + appData["bundleId"] = + QString(bundle_id_str); + free(bundle_id_str); + } + } + + // Get display name + plist_t display_name = plist_dict_get_item( + app_info, "CFBundleDisplayName"); + if (display_name && + plist_get_node_type(display_name) == + PLIST_STRING) { + char *display_name_str = nullptr; + plist_get_string_val(display_name, + &display_name_str); + if (display_name_str) { + appData["displayName"] = + QString(display_name_str); + free(display_name_str); + } + } + + // Get version + plist_t version = plist_dict_get_item( + app_info, "CFBundleShortVersionString"); + if (version && + plist_get_node_type(version) == PLIST_STRING) { + char *version_str = nullptr; + plist_get_string_val(version, &version_str); + if (version_str) { + appData["version"] = QString(version_str); + free(version_str); + } + } + + appData["type"] = appType; + + if (!appData["bundleId"].toString().isEmpty()) { + apps.append(appData); + } + } + } + plist_free(apps_plist); + } + plist_free(client_opts); + } + + instproxy_client_free(instproxy); + lockdownd_client_free(lockdownClient); + + result["apps"] = apps; + result["success"] = true; + + } catch (const std::exception &e) { + if (instproxy) + instproxy_client_free(instproxy); + if (lockdownClient) + lockdownd_client_free(lockdownClient); + if (lockdowndService) + lockdownd_service_descriptor_free(lockdowndService); + + result["error"] = QString("Exception: %1").arg(e.what()); + } + + return result; + }); + + m_watcher->setFuture(future); +} + +void InstalledAppsWidget::onAppsDataReady() +{ + QVariantMap result = m_watcher->result(); + m_progressBar->setVisible(false); + + if (!result.value("success", false).toBool()) { + showErrorState(result.value("error", "Unknown error").toString()); + return; + } + + QVariantList apps = result.value("apps").toList(); + if (apps.isEmpty()) { + m_contentLabel->setText("No apps found"); + return; + } + + // Sort apps by display name + std::sort(apps.begin(), apps.end(), + [](const QVariant &a, const QVariant &b) { + QString nameA = a.toMap().value("displayName").toString(); + QString nameB = b.toMap().value("displayName").toString(); + if (nameA.isEmpty()) + nameA = a.toMap().value("bundleId").toString(); + if (nameB.isEmpty()) + nameB = b.toMap().value("bundleId").toString(); + return nameA.compare(nameB, Qt::CaseInsensitive) < 0; + }); + + // Clear existing tabs + qDeleteAll(m_appTabs); + m_appTabs.clear(); + m_selectedTab = nullptr; + + // Create tabs for each app + for (const QVariant &appVariant : apps) { + QVariantMap appData = appVariant.toMap(); + QString displayName = appData.value("displayName").toString(); + QString bundleId = appData.value("bundleId").toString(); + QString version = appData.value("version").toString(); + QString appType = appData.value("type").toString(); + + if (displayName.isEmpty()) { + displayName = bundleId; + } + + // Create tab name with type indicator + QString tabName = displayName; + if (appType == "System") { + tabName += " (System)"; + } + + createAppTab(tabName, bundleId, version); + } + + m_contentLabel->setText( + QString("Found %1 installed apps").arg(apps.count())); + + // Select first tab if available + if (!m_appTabs.isEmpty()) { + selectAppTab(m_appTabs.first()); + } +} + +void InstalledAppsWidget::createAppTab(const QString &appName, + const QString &bundleId, + const QString &version) +{ + AppTabWidget *tabWidget = + new AppTabWidget(appName, bundleId, version, this); + connect(tabWidget, &AppTabWidget::clicked, this, + &InstalledAppsWidget::onAppTabClicked); + + // Remove the stretch before adding the new tab + m_tabLayout->removeItem(m_tabLayout->itemAt(m_tabLayout->count() - 1)); + + m_tabLayout->addWidget(tabWidget); + m_tabLayout->addStretch(); // Add stretch back at the end + + m_appTabs.append(tabWidget); +} + +void InstalledAppsWidget::onAppTabClicked() +{ + AppTabWidget *clickedTab = qobject_cast(sender()); + if (clickedTab) { + selectAppTab(clickedTab); + } +} + +void InstalledAppsWidget::selectAppTab(AppTabWidget *tab) +{ + // Deselect previous tab + if (m_selectedTab) { + m_selectedTab->setSelected(false); + } + + // Select new tab + m_selectedTab = tab; + tab->setSelected(true); + + // Update content + QString bundleId = tab->getBundleId(); + QString version = tab->getVersion(); + QString appName = tab->getAppName(); + + // Remove the (System) suffix for display + QString displayName = appName; + displayName.remove(" (System)"); + + QString content = QString("

%1

" + "

Bundle ID: %2

" + "

Version: %3

" + "
") + .arg(displayName, bundleId, + version.isEmpty() ? "Unknown" : version); + + m_contentLabel->setText(content); + m_contentLabel->setTextFormat(Qt::RichText); + m_contentLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); + m_contentLabel->setWordWrap(true); + + // Load app container data + loadAppContainer(bundleId); +} + +void InstalledAppsWidget::filterApps(const QString &searchText) +{ + QString lowerSearchText = searchText.toLower(); + + for (AppTabWidget *tab : m_appTabs) { + bool shouldShow = false; + + if (lowerSearchText.isEmpty()) { + shouldShow = true; + } else { + // Search in app name and bundle ID + QString appName = tab->getAppName().toLower(); + QString bundleId = tab->getBundleId().toLower(); + + shouldShow = appName.contains(lowerSearchText) || + bundleId.contains(lowerSearchText); + } + + tab->setVisible(shouldShow); + } +} + +void InstalledAppsWidget::loadAppContainer(const QString &bundleId) +{ + if (!m_device || !m_device->device) { + return; + } + + // Clear previous container data + QLayoutItem *item; + while ((item = m_containerLayout->takeAt(0)) != nullptr) { + if (item->widget()) { + item->widget()->deleteLater(); + } + delete item; + } + + // Show loading state + QLabel *loadingLabel = new QLabel("Loading app container..."); + loadingLabel->setStyleSheet("color: #666; font-style: italic;"); + m_containerLayout->addWidget(loadingLabel); + m_containerScrollArea->setVisible(true); + + // Find container title and make it visible + QVBoxLayout *contentLayout = + qobject_cast(m_contentWidget->layout()); + if (contentLayout && contentLayout->count() > 1) { + QLayoutItem *titleItem = contentLayout->itemAt(1); + if (titleItem && titleItem->widget()) { + titleItem->widget()->setVisible(true); + } + } + + QFuture future = QtConcurrent::run([this, bundleId]() + -> QVariantMap { + QVariantMap result; + + afc_client_t afcClient = nullptr; + lockdownd_client_t lockdownClient = nullptr; + lockdownd_service_descriptor_t lockdowndService = nullptr; + house_arrest_client_t houseArrestClient = nullptr; + + try { + if (lockdownd_client_new_with_handshake( + m_device->device, &lockdownClient, APP_LABEL) != + LOCKDOWN_E_SUCCESS) { + result["error"] = "Could not connect to lockdown service"; + return result; + } + + if (lockdownd_start_service( + lockdownClient, "com.apple.mobile.house_arrest", + &lockdowndService) != LOCKDOWN_E_SUCCESS) { + result["error"] = "Could not start house arrest service"; + lockdownd_client_free(lockdownClient); + return result; + } + + if (house_arrest_client_new(m_device->device, lockdowndService, + &houseArrestClient) != + HOUSE_ARREST_E_SUCCESS) { + result["error"] = "Could not connect to house arrest"; + lockdownd_service_descriptor_free(lockdowndService); + lockdownd_client_free(lockdownClient); + return result; + } + + lockdownd_service_descriptor_free(lockdowndService); + lockdowndService = nullptr; + + // Send vendor container command + if (house_arrest_send_command(houseArrestClient, "VendContainer", + bundleId.toUtf8().constData()) != + HOUSE_ARREST_E_SUCCESS) { + result["error"] = "Could not send VendContainer command"; + house_arrest_client_free(houseArrestClient); + lockdownd_client_free(lockdownClient); + return result; + } + + // Get result + plist_t dict = nullptr; + if (house_arrest_get_result(houseArrestClient, &dict) != + HOUSE_ARREST_E_SUCCESS || + !dict) { + result["error"] = "App container not available for this app"; + house_arrest_client_free(houseArrestClient); + lockdownd_client_free(lockdownClient); + return result; + } + + // Check for error in response + plist_t error_node = plist_dict_get_item(dict, "Error"); + if (error_node) { + char *error_str = nullptr; + plist_get_string_val(error_node, &error_str); + if (error_str) { + result["error"] = + QString("Container access denied: %1").arg(error_str); + free(error_str); + } else { + result["error"] = "Container access denied"; + } + plist_free(dict); + house_arrest_client_free(houseArrestClient); + lockdownd_client_free(lockdownClient); + return result; + } + + plist_free(dict); + + // Get AFC client for file access + if (afc_client_new_from_house_arrest_client( + houseArrestClient, &afcClient) != AFC_E_SUCCESS) { + result["error"] = + "Could not create AFC client for app container"; + house_arrest_client_free(houseArrestClient); + lockdownd_client_free(lockdownClient); + return result; + } + + // List root directory contents + char **list = nullptr; + if (afc_read_directory(afcClient, "/", &list) != AFC_E_SUCCESS) { + result["error"] = "Could not read app container directory"; + afc_client_free(afcClient); + house_arrest_client_free(houseArrestClient); + lockdownd_client_free(lockdownClient); + return result; + } + + QStringList files; + if (list) { + for (int i = 0; list[i]; i++) { + QString fileName = QString::fromUtf8(list[i]); + if (fileName != "." && fileName != "..") { + files.append(fileName); + } + } + afc_dictionary_free(list); + } + + result["files"] = files; + result["success"] = true; + + afc_client_free(afcClient); + house_arrest_client_free(houseArrestClient); + lockdownd_client_free(lockdownClient); + + } catch (const std::exception &e) { + if (afcClient) + afc_client_free(afcClient); + if (houseArrestClient) + house_arrest_client_free(houseArrestClient); + if (lockdownClient) + lockdownd_client_free(lockdownClient); + if (lockdowndService) + lockdownd_service_descriptor_free(lockdowndService); + + result["error"] = QString("Exception: %1").arg(e.what()); + } + + return result; + }); + + m_containerWatcher->setFuture(future); +} + +void InstalledAppsWidget::onContainerDataReady() +{ + QVariantMap result = m_containerWatcher->result(); + + // Clear loading state + QLayoutItem *item; + while ((item = m_containerLayout->takeAt(0)) != nullptr) { + if (item->widget()) { + item->widget()->deleteLater(); + } + delete item; + } + + if (!result.value("success", false).toBool()) { + QLabel *errorLabel = new QLabel("No data available for this app"); + errorLabel->setStyleSheet( + "color: #999; font-style: italic; text-align: center;"); + errorLabel->setAlignment(Qt::AlignCenter); + m_containerLayout->addWidget(errorLabel); + return; + } + + QStringList files = result.value("files").toStringList(); + if (files.isEmpty()) { + QLabel *emptyLabel = new QLabel("App container is empty"); + emptyLabel->setStyleSheet("color: #999; font-style: italic;"); + m_containerLayout->addWidget(emptyLabel); + return; + } + + // Sort files/directories + files.sort(); + + // Add files/directories to the container view + for (const QString &fileName : files) { + QLabel *fileLabel = new QLabel(); + + // Determine if it's likely a directory (simple heuristic) + QString displayText = fileName; + QIcon icon; + if (!fileName.contains('.') || fileName.endsWith("/")) { + icon = this->style()->standardIcon(QStyle::SP_DirIcon); + displayText = "📁 " + fileName; + } else { + icon = this->style()->standardIcon(QStyle::SP_FileIcon); + displayText = "📄 " + fileName; + } + + fileLabel->setText(displayText); + fileLabel->setStyleSheet("QLabel { " + " padding: 4px 8px; " + " border: 1px solid transparent; " + " border-radius: 3px; " + " font-family: monospace; " + " font-size: 13px; " + "} " + "QLabel:hover { " + " background-color: #f0f0f0; " + " border: 1px solid #ddd; " + "}"); + fileLabel->setWordWrap(true); + + m_containerLayout->addWidget(fileLabel); + } + + // Add stretch to push items to top + m_containerLayout->addStretch(); +} diff --git a/src/installedappswidget.h b/src/installedappswidget.h new file mode 100644 index 0000000..d7f24a9 --- /dev/null +++ b/src/installedappswidget.h @@ -0,0 +1,103 @@ +#ifndef INSTALLEDAPPSWIDGET_H +#define INSTALLEDAPPSWIDGET_H + +#include "iDescriptor.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Custom App Tab Widget +class AppTabWidget : public QWidget +{ + Q_OBJECT + +public: + AppTabWidget(const QString &appName, const QString &bundleId, + const QString &version, QWidget *parent = nullptr); + + void setSelected(bool selected); + bool isSelected() const { return m_selected; } + + QString getBundleId() const { return m_bundleId; } + QString getAppName() const { return m_appName; } + QString getVersion() const { return m_version; } + +signals: + void clicked(); + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + // TODO override? + void enterEvent(QEnterEvent *event); + void leaveEvent(QEvent *event) override; + +private: + void fetchAppIcon(); + + QString m_appName; + QString m_bundleId; + QString m_version; + QPixmap m_icon; + bool m_selected = false; + bool m_hovered = false; +}; + +class InstalledAppsWidget : public QWidget +{ + Q_OBJECT + +public: + explicit InstalledAppsWidget(iDescriptorDevice *device, + QWidget *parent = nullptr); + +private slots: + void onAppsDataReady(); + void onAppTabClicked(); + void onContainerDataReady(); + +private: + void setupUI(); + void fetchInstalledApps(); + void createAppTab(const QString &appName, const QString &bundleId, + const QString &version); + void showLoadingState(); + void showErrorState(const QString &error); + void selectAppTab(AppTabWidget *tab); + void filterApps(const QString &searchText); + void loadAppContainer(const QString &bundleId); + + iDescriptorDevice *m_device; + QHBoxLayout *m_mainLayout; + QLineEdit *m_searchEdit; + QScrollArea *m_tabScrollArea; + QWidget *m_tabContainer; + QVBoxLayout *m_tabLayout; + QWidget *m_contentWidget; + QLabel *m_contentLabel; + QProgressBar *m_progressBar; + QScrollArea *m_containerScrollArea; + QWidget *m_containerWidget; + QVBoxLayout *m_containerLayout; + QFutureWatcher *m_watcher; + QFutureWatcher *m_containerWatcher; + + // App data storage + QList m_appTabs; + AppTabWidget *m_selectedTab = nullptr; +}; + +#endif // INSTALLEDAPPSWIDGET_H \ No newline at end of file diff --git a/src/settingsmanager.cpp b/src/settingsmanager.cpp index ecc4098..4148d69 100644 --- a/src/settingsmanager.cpp +++ b/src/settingsmanager.cpp @@ -1,4 +1,5 @@ #include "settingsmanager.h" +#include #include #define DEFAULT_DEVDISKIMGPATH "./devdiskimages" @@ -11,12 +12,136 @@ SettingsManager *SettingsManager::sharedInstance() SettingsManager::SettingsManager(QObject *parent) : QObject{parent} { - m_settings = new QSettings(this); + + // Clean up any invalid favorite places on startup + cleanupFavoritePlaces(); } QString SettingsManager::devdiskimgpath() const { return m_settings->value("devdiskimgpath", DEFAULT_DEVDISKIMGPATH) .toString(); +} + +void SettingsManager::saveFavoritePlace(const QString &path, + const QString &alias) +{ + if (path.isEmpty() || alias.isEmpty()) { + qDebug() << "Cannot save favorite place: path or alias is empty"; + return; + } + + // Use a key that encodes the path properly + QString key = + "favorite_places/" + QString::fromLatin1(path.toUtf8().toBase64()); + m_settings->setValue(key, QStringList() << path << alias); + m_settings->sync(); + + qDebug() << "Saved favorite place:" << alias << "(" << path << ")"; + emit favoritePlacesChanged(); +} + +void SettingsManager::removeFavoritePlace(const QString &path) +{ + // Use the same encoding as in saveFavoritePlace + QString key = + "favorite_places/" + QString::fromLatin1(path.toUtf8().toBase64()); + if (m_settings->contains(key)) { + m_settings->remove(key); + m_settings->sync(); + qDebug() << "Removed favorite place:" << path; + emit favoritePlacesChanged(); + } +} + +QList> SettingsManager::getFavoritePlaces() const +{ + QList> favorites; + + // Get all keys that start with "favorite_places/" + QStringList allKeys = m_settings->allKeys(); + QStringList favoriteKeys = allKeys.filter("favorite_places/"); + + qDebug() << "Found favorite keys:" << favoriteKeys; + + for (const QString &key : favoriteKeys) { + QStringList value = m_settings->value(key).toStringList(); + if (value.size() >= 2) { + QString path = value[0]; + QString alias = value[1]; + if (!path.isEmpty() && !alias.isEmpty()) { + favorites.append(qMakePair(path, alias)); + qDebug() << "Loaded favorite:" << alias << "->" << path; + } + } + } + + // Sort by alias for consistent ordering + std::sort( + favorites.begin(), favorites.end(), + [](const QPair &a, const QPair &b) { + return a.second.toLower() < b.second.toLower(); + }); + + return favorites; +} + +bool SettingsManager::isFavoritePlace(const QString &path) const +{ + QString key = + "favorite_places/" + QString::fromLatin1(path.toUtf8().toBase64()); + return m_settings->contains(key); +} + +QString SettingsManager::getFavoritePlaceAlias(const QString &path) const +{ + QString key = + "favorite_places/" + QString::fromLatin1(path.toUtf8().toBase64()); + QStringList value = m_settings->value(key).toStringList(); + if (value.size() >= 2) { + return value[1]; // Return alias + } + return QString(); +} + +void SettingsManager::clearFavoritePlaces() +{ + // Get all keys that start with "favorite_places/" and remove them + QStringList allKeys = m_settings->allKeys(); + QStringList favoriteKeys = allKeys.filter("favorite_places/"); + + for (const QString &key : favoriteKeys) { + m_settings->remove(key); + } + + m_settings->sync(); + + qDebug() << "Cleared all favorite places"; + emit favoritePlacesChanged(); +} + +void SettingsManager::cleanupFavoritePlaces() +{ + // Get all keys that start with "favorite_places/" and clean them up + QStringList allKeys = m_settings->allKeys(); + QStringList favoriteKeys = allKeys.filter("favorite_places/"); + QStringList keysToRemove; + + for (const QString &key : favoriteKeys) { + QStringList value = m_settings->value(key).toStringList(); + if (value.size() < 2 || value[0].isEmpty() || value[1].isEmpty()) { + keysToRemove.append(key); + } + } + + for (const QString &key : keysToRemove) { + qDebug() << "Removing invalid favorite place key:" << key; + m_settings->remove(key); + } + + if (!keysToRemove.isEmpty()) { + m_settings->sync(); + emit favoritePlacesChanged(); + } } \ No newline at end of file diff --git a/src/settingsmanager.h b/src/settingsmanager.h index 29f0ef9..f11a77e 100644 --- a/src/settingsmanager.h +++ b/src/settingsmanager.h @@ -2,22 +2,40 @@ #define SETTINGSMANAGER_H #include +#include #include +#include +#include class SettingsManager : public QObject { Q_OBJECT + public: - explicit SettingsManager(QObject *parent = nullptr); static SettingsManager *sharedInstance(); -signals: - -public slots: + // Existing methods QString devdiskimgpath() const; + // Favorite Places API + void saveFavoritePlace(const QString &path, const QString &alias); + void removeFavoritePlace(const QString &path); + QList> + getFavoritePlaces() const; // Returns (path, alias) pairs + bool isFavoritePlace(const QString &path) const; + QString getFavoritePlaceAlias(const QString &path) const; + void clearFavoritePlaces(); + +signals: + void favoritePlacesChanged(); + private: + explicit SettingsManager(QObject *parent = nullptr); QSettings *m_settings; + + void cleanupFavoritePlaces(); + + static const QString FAVORITE_PREFIX; }; #endif // SETTINGSMANAGER_H