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.
This commit is contained in:
uncor3
2025-09-27 20:27:22 +00:00
parent 3b9df7577b
commit b4379505c7
16 changed files with 1538 additions and 134 deletions
+1
View File
@@ -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)
-58
View File
@@ -28,8 +28,6 @@
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QPainter>
#include <QPainterPath>
#include <QPixmap>
@@ -45,62 +43,6 @@
#include <QWidget>
#include <QtConcurrent/QtConcurrent>
// Callback: void(QPixmap)
// TODO : move to utils
void fetchAppIconFromApple(const QString &bundleId,
std::function<void(const QPixmap &)> 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);
@@ -0,0 +1,61 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QPixmap>
#include <QtConcurrent/QtConcurrent>
void fetchAppIconFromApple(const QString &bundleId,
std::function<void(const QPixmap &)> 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();
});
});
}
+35
View File
@@ -0,0 +1,35 @@
#include "../../iDescriptor.h"
#include <QDebug>
#include <libimobiledevice/afc.h>
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
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);
// }
}
+16 -2
View File
@@ -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);
+1 -20
View File
@@ -2,6 +2,7 @@
#include "batterywidget.h"
#include "diskusagewidget.h"
#include "fileexplorerwidget.h"
#include "iDescriptor-ui.h"
#include "iDescriptor.h"
#include <QDebug>
#include <QGraphicsPixmapItem>
@@ -20,26 +21,6 @@
#include <QTimer>
#include <QVBoxLayout>
// 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)
{
+33
View File
@@ -0,0 +1,33 @@
// #ifndef DEVICELISTENER_H
// #define DEVICELISTENER_H
// #include <QBluetoothDeviceInfo>
// #include <QLowEnergyAdvertisingData>
// #include <QLowEnergyController>
// #include <QLowEnergyServiceData>
// #include <QObject>
// 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
+10 -7
View File
@@ -3,6 +3,7 @@
#include "fileexplorerwidget.h"
#include "gallerywidget.h"
#include "iDescriptor.h"
#include "installedappswidget.h"
#include <QDebug>
#include <QTabWidget>
#include <QVBoxLayout>
@@ -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;
}
+233 -40
View File
@@ -1,68 +1,58 @@
#include "fileexplorerwidget.h"
#include "iDescriptor.h"
#include "mediapreviewdialog.h"
#include "settingsmanager.h"
#include <QDebug>
#include <QDesktopServices>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QIcon>
#include <QInputDialog>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QSignalBlocker>
#include <QSplitter>
#include <QTreeWidget>
#include <QVariant>
#include <libimobiledevice/afc.h>
#include <libimobiledevice/libimobiledevice.h>
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<bool, QString> 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<SidebarItemData>().first;
QString path = item->data(0, Qt::UserRole).value<SidebarItemData>().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<QPair<QString, QString>> 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();
}
+26
View File
@@ -3,12 +3,15 @@
#include "iDescriptor.h"
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QListWidget>
#include <QMenu>
#include <QPushButton>
#include <QSplitter>
#include <QStack>
#include <QString>
#include <QTreeWidget>
#include <QVBoxLayout>
#include <QWidget>
#include <libimobiledevice/afc.h>
@@ -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<QString> 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);
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <QGraphicsView>
// 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);
}
};
+10 -2
View File
@@ -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);
void parseDeviceBattery(PlistNavigator &ioreg, DeviceInfo &d);
void fetchAppIconFromApple(const QString &bundleId,
std::function<void(const QPixmap &)> callback,
QObject *context);
afc_error_t afc2_client_new(idevice_t device, afc_client_t *afc);
+840
View File
@@ -0,0 +1,840 @@
#include "installedappswidget.h"
#include "iDescriptor.h"
#include <QAction>
#include <QApplication>
#include <QDebug>
#include <QEnterEvent>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLineEdit>
#include <QStyle>
#include <QtConcurrent/QtConcurrent>
#include <libimobiledevice/afc.h>
#include <libimobiledevice/house_arrest.h>
#include <libimobiledevice/installation_proxy.h>
#include <libimobiledevice/lockdown.h>
#include <plist/plist.h>
// 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<QVariantMap>(this);
m_containerWatcher = new QFutureWatcher<QVariantMap>(this);
setupUI();
connect(m_watcher, &QFutureWatcher<QVariantMap>::finished, this,
&InstalledAppsWidget::onAppsDataReady);
connect(m_containerWatcher, &QFutureWatcher<QVariantMap>::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<QVariantMap> 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<AppTabWidget *>(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("<h2>%1</h2>"
"<p><b>Bundle ID:</b> %2</p>"
"<p><b>Version:</b> %3</p>"
"<hr>")
.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<QVBoxLayout *>(m_contentWidget->layout());
if (contentLayout && contentLayout->count() > 1) {
QLayoutItem *titleItem = contentLayout->itemAt(1);
if (titleItem && titleItem->widget()) {
titleItem->widget()->setVisible(true);
}
}
QFuture<QVariantMap> 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();
}
+103
View File
@@ -0,0 +1,103 @@
#ifndef INSTALLEDAPPSWIDGET_H
#define INSTALLEDAPPSWIDGET_H
#include "iDescriptor.h"
#include <QEnterEvent>
#include <QFrame>
#include <QFutureWatcher>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QPainter>
#include <QPainterPath>
#include <QPixmap>
#include <QProgressBar>
#include <QPushButton>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QWidget>
// 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<QVariantMap> *m_watcher;
QFutureWatcher<QVariantMap> *m_containerWatcher;
// App data storage
QList<AppTabWidget *> m_appTabs;
AppTabWidget *m_selectedTab = nullptr;
};
#endif // INSTALLEDAPPSWIDGET_H
+126 -1
View File
@@ -1,4 +1,5 @@
#include "settingsmanager.h"
#include <QDebug>
#include <QSettings>
#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<QPair<QString, QString>> SettingsManager::getFavoritePlaces() const
{
QList<QPair<QString, QString>> 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<QString, QString> &a, const QPair<QString, QString> &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();
}
}
+22 -4
View File
@@ -2,22 +2,40 @@
#define SETTINGSMANAGER_H
#include <QObject>
#include <QPair>
#include <QSettings>
#include <QString>
#include <QStringList>
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<QPair<QString, QString>>
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