mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-22 03:45:51 +08:00
915 lines
32 KiB
C++
915 lines
32 KiB
C++
/*
|
|
* iDescriptor: A free and open-source idevice management tool.
|
|
*
|
|
* Copyright (C) 2025 Uncore <https://github.com/uncor3>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "installedappswidget.h"
|
|
#include "afcexplorerwidget.h"
|
|
#include "iDescriptor-ui.h"
|
|
#include "iDescriptor.h"
|
|
#include "qprocessindicator.h"
|
|
#include "zlineedit.h"
|
|
#include <QAction>
|
|
#include <QApplication>
|
|
#include <QDebug>
|
|
#include <QEnterEvent>
|
|
#include <QGroupBox>
|
|
#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::AppTabWidget(const QString &appName, const QString &bundleId,
|
|
const QString &version, QWidget *parent)
|
|
: QGroupBox(parent), m_appName(appName), m_bundleId(bundleId),
|
|
m_version(version), m_selected(false)
|
|
{
|
|
setFixedHeight(60);
|
|
setMinimumWidth(100);
|
|
setCursor(Qt::PointingHandCursor);
|
|
|
|
setupUI();
|
|
fetchAppIcon();
|
|
}
|
|
|
|
void AppTabWidget::fetchAppIcon()
|
|
{
|
|
::fetchAppIconFromApple(
|
|
m_networkManager, 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_iconLabel->setPixmap(rounded);
|
|
}
|
|
});
|
|
}
|
|
|
|
void AppTabWidget::setSelected(bool selected)
|
|
{
|
|
m_selected = selected;
|
|
updateStyles();
|
|
}
|
|
|
|
void AppTabWidget::setupUI()
|
|
{
|
|
QHBoxLayout *mainLayout = new QHBoxLayout(this);
|
|
mainLayout->setContentsMargins(10, 8, 10, 8);
|
|
mainLayout->setSpacing(10);
|
|
// m_defaultBg = this->palette().color(QPalette::Window);
|
|
// Icon label
|
|
m_iconLabel = new QLabel();
|
|
m_iconLabel->setFixedSize(32, 32);
|
|
m_iconLabel->setScaledContents(true);
|
|
|
|
QPixmap placeholderIcon = QApplication::style()
|
|
->standardIcon(QStyle::SP_ComputerIcon)
|
|
.pixmap(32, 32);
|
|
m_iconLabel->setPixmap(placeholderIcon);
|
|
mainLayout->addWidget(m_iconLabel);
|
|
|
|
// Text container
|
|
QVBoxLayout *textLayout = new QVBoxLayout();
|
|
textLayout->setContentsMargins(0, 0, 0, 0);
|
|
textLayout->setSpacing(2);
|
|
|
|
// App name label
|
|
m_nameLabel = new QLabel();
|
|
QFont nameFont = m_nameLabel->font();
|
|
nameFont.setWeight(QFont::Medium);
|
|
m_nameLabel->setFont(nameFont);
|
|
|
|
QString displayText = m_appName;
|
|
if (displayText.length() > 20) {
|
|
displayText = displayText.left(17) + "...";
|
|
}
|
|
m_nameLabel->setText(displayText);
|
|
textLayout->addWidget(m_nameLabel);
|
|
|
|
// Version label
|
|
if (!m_version.isEmpty()) {
|
|
m_versionLabel = new QLabel(m_version);
|
|
m_versionLabel->setStyleSheet("font-size: 11px;");
|
|
textLayout->addWidget(m_versionLabel);
|
|
} else {
|
|
m_versionLabel = nullptr;
|
|
}
|
|
|
|
mainLayout->addLayout(textLayout);
|
|
mainLayout->addStretch();
|
|
|
|
updateStyles();
|
|
}
|
|
|
|
void AppTabWidget::mousePressEvent(QMouseEvent *event)
|
|
{
|
|
Q_UNUSED(event)
|
|
emit clicked();
|
|
}
|
|
|
|
void AppTabWidget::updateStyles()
|
|
{
|
|
QString style;
|
|
QColor bgColor = isDarkMode() ? qApp->palette().color(QPalette::Light)
|
|
: qApp->palette().color(QPalette::Dark);
|
|
if (m_selected) {
|
|
style = "QGroupBox { background-color: " + COLOR_ACCENT_BLUE.name() +
|
|
"; border-radius: "
|
|
"10px; border : 1px solid " +
|
|
bgColor.lighter().name() + "; }";
|
|
} else {
|
|
style = "QGroupBox { background-color: " + bgColor.name() +
|
|
"; border-radius: 10px; border: 1px solid " +
|
|
bgColor.lighter().name() + "; }";
|
|
}
|
|
setStyleSheet(style);
|
|
}
|
|
|
|
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);
|
|
setStyleSheet("InstalledAppsWidget { background: transparent; }");
|
|
fetchInstalledApps();
|
|
}
|
|
|
|
InstalledAppsWidget::~InstalledAppsWidget() { cleanupHouseArrestClients(); }
|
|
|
|
void InstalledAppsWidget::setupUI()
|
|
{
|
|
m_mainLayout = new QHBoxLayout(this);
|
|
m_mainLayout->setContentsMargins(0, 0, 0, 0);
|
|
m_mainLayout->setSpacing(0);
|
|
|
|
// Create stacked widget for different states
|
|
m_stackedWidget = new QStackedWidget(this);
|
|
m_mainLayout->addWidget(m_stackedWidget);
|
|
|
|
// Create loading widget
|
|
createLoadingWidget();
|
|
|
|
// Create error widget
|
|
createErrorWidget();
|
|
|
|
// Create content widget
|
|
createContentWidget();
|
|
|
|
// Start in loading state
|
|
showLoadingState();
|
|
|
|
connect(qApp, &QApplication::paletteChanged, this, [this]() {
|
|
for (AppTabWidget *tab : m_appTabs) {
|
|
tab->updateStyles();
|
|
}
|
|
});
|
|
}
|
|
|
|
void InstalledAppsWidget::showLoadingState()
|
|
{
|
|
m_stackedWidget->setCurrentWidget(m_loadingWidget);
|
|
}
|
|
|
|
void InstalledAppsWidget::showErrorState(const QString &error)
|
|
{
|
|
m_errorLabel->setText(QString("Error loading apps: %1").arg(error));
|
|
m_stackedWidget->setCurrentWidget(m_errorWidget);
|
|
}
|
|
|
|
void InstalledAppsWidget::createLoadingWidget()
|
|
{
|
|
m_loadingWidget = new QWidget();
|
|
QVBoxLayout *loadingLayout = new QVBoxLayout(m_loadingWidget);
|
|
loadingLayout->setAlignment(Qt::AlignCenter);
|
|
|
|
QProcessIndicator *spinner = new QProcessIndicator();
|
|
spinner->setType(QProcessIndicator::line_rotate);
|
|
spinner->setFixedSize(48, 48);
|
|
spinner->start();
|
|
loadingLayout->addWidget(spinner, 0, Qt::AlignCenter);
|
|
|
|
QLabel *loadingLabel = new QLabel("Loading installed apps...");
|
|
loadingLabel->setAlignment(Qt::AlignCenter);
|
|
loadingLabel->setStyleSheet(
|
|
"font-size: 14px; color: #666; margin-top: 10px;");
|
|
loadingLayout->addWidget(loadingLabel);
|
|
|
|
m_stackedWidget->addWidget(m_loadingWidget);
|
|
}
|
|
|
|
void InstalledAppsWidget::createErrorWidget()
|
|
{
|
|
m_errorWidget = new QWidget();
|
|
QVBoxLayout *errorLayout = new QVBoxLayout(m_errorWidget);
|
|
errorLayout->setAlignment(Qt::AlignCenter);
|
|
|
|
m_errorLabel = new QLabel();
|
|
m_errorLabel->setAlignment(Qt::AlignCenter);
|
|
m_errorLabel->setStyleSheet(
|
|
"font-size: 14px; color: #d32f2f; margin: 20px;");
|
|
m_errorLabel->setWordWrap(true);
|
|
errorLayout->addWidget(m_errorLabel);
|
|
|
|
QPushButton *retryButton = new QPushButton("Retry");
|
|
retryButton->setFixedSize(100, 30);
|
|
connect(retryButton, &QPushButton::clicked, this,
|
|
&InstalledAppsWidget::fetchInstalledApps);
|
|
errorLayout->addWidget(retryButton, 0, Qt::AlignCenter);
|
|
|
|
m_stackedWidget->addWidget(m_errorWidget);
|
|
}
|
|
|
|
void InstalledAppsWidget::createContentWidget()
|
|
{
|
|
m_contentWidget = new QWidget();
|
|
QHBoxLayout *contentLayout = new QHBoxLayout(m_contentWidget);
|
|
contentLayout->setContentsMargins(0, 0, 0, 0);
|
|
contentLayout->setSpacing(0);
|
|
|
|
// Create main splitter
|
|
m_splitter = new ModernSplitter(Qt::Horizontal, m_contentWidget);
|
|
m_splitter->setChildrenCollapsible(false);
|
|
contentLayout->addWidget(m_splitter);
|
|
|
|
// Left side - App list
|
|
createLeftPanel();
|
|
|
|
// Right side - Content area
|
|
createRightPanel();
|
|
|
|
// Set initial splitter sizes (400px for tabs, rest for content)
|
|
m_splitter->setSizes({400, 600});
|
|
|
|
// Connect signals
|
|
connect(m_searchEdit, &QLineEdit::textChanged, this,
|
|
&InstalledAppsWidget::filterApps);
|
|
connect(m_fileSharingCheckBox, &QCheckBox::toggled, this,
|
|
&InstalledAppsWidget::onFileSharingFilterChanged);
|
|
|
|
m_stackedWidget->addWidget(m_contentWidget);
|
|
}
|
|
|
|
// todo: move to services
|
|
void InstalledAppsWidget::fetchInstalledApps()
|
|
{
|
|
if (!m_device || !m_device->device) {
|
|
showErrorState("Invalid device");
|
|
return;
|
|
}
|
|
|
|
QFuture<QVariantMap> future = QtConcurrent::run([this]() -> QVariantMap {
|
|
QVariantMap result;
|
|
QVariantList apps;
|
|
|
|
// result["success"] = true;
|
|
// result["apps"] = apps;
|
|
// return result;
|
|
|
|
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_array_append_item(
|
|
return_attrs, plist_new_string("UIFileSharingEnabled"));
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Get file sharing enabled status
|
|
plist_t file_sharing = plist_dict_get_item(
|
|
app_info, "UIFileSharingEnabled");
|
|
if (file_sharing &&
|
|
plist_get_node_type(file_sharing) ==
|
|
PLIST_BOOLEAN) {
|
|
uint8_t file_sharing_enabled = 0;
|
|
plist_get_bool_val(file_sharing,
|
|
&file_sharing_enabled);
|
|
appData["fileSharingEnabled"] =
|
|
(file_sharing_enabled != 0);
|
|
} else {
|
|
appData["fileSharingEnabled"] = false;
|
|
}
|
|
|
|
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();
|
|
|
|
if (!result.value("success", false).toBool()) {
|
|
showErrorState(result.value("error", "Unknown error").toString());
|
|
return;
|
|
}
|
|
|
|
QVariantList apps = result.value("apps").toList();
|
|
if (apps.isEmpty()) {
|
|
showErrorState("No apps found");
|
|
return;
|
|
}
|
|
|
|
// Switch to content view once data is loaded
|
|
m_stackedWidget->setCurrentWidget(m_contentWidget);
|
|
|
|
// 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();
|
|
bool fileSharingEnabled =
|
|
appData.value("fileSharingEnabled", false).toBool();
|
|
|
|
// Filter by file sharing status if checkbox is checked
|
|
if (m_fileSharingCheckBox->isChecked() && !fileSharingEnabled) {
|
|
continue;
|
|
}
|
|
|
|
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);
|
|
|
|
QString bundleId = tab->getBundleId();
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
/*
|
|
FIXME: maybe we better have this in servicemanager,
|
|
for now it's ok as it's only used here
|
|
*/
|
|
void InstalledAppsWidget::loadAppContainer(const QString &bundleId)
|
|
{
|
|
if (!m_device || !m_device->device) {
|
|
return;
|
|
}
|
|
|
|
// Clean up previous house arrest clients before creating new ones
|
|
cleanupHouseArrestClients();
|
|
|
|
// Clear previous container data
|
|
QLayoutItem *item;
|
|
while ((item = m_containerLayout->takeAt(0)) != nullptr) {
|
|
if (item->widget()) {
|
|
item->widget()->deleteLater();
|
|
}
|
|
delete item;
|
|
}
|
|
|
|
// Create a centered loading widget
|
|
QWidget *loadingWidget = new QWidget();
|
|
QVBoxLayout *loadingLayout = new QVBoxLayout(loadingWidget);
|
|
loadingLayout->setAlignment(Qt::AlignCenter);
|
|
|
|
QProcessIndicator *l = new QProcessIndicator();
|
|
l->setType(QProcessIndicator::line_rotate);
|
|
l->setFixedSize(32, 32);
|
|
l->start();
|
|
loadingLayout->addWidget(l, 0, Qt::AlignCenter);
|
|
|
|
m_containerLayout->addWidget(loadingWidget);
|
|
|
|
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, "VendDocuments",
|
|
// if (house_arrest_send_command(houseArrestClient,
|
|
// "VendDocuments",
|
|
bundleId.toUtf8().constData()) != HOUSE_ARREST_E_SUCCESS) {
|
|
result["error"] = "Could not send VendDocuments 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, "/Documents", &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 != "..") {
|
|
qDebug() << "Found file:" << fileName;
|
|
files.append(fileName);
|
|
}
|
|
}
|
|
afc_dictionary_free(list);
|
|
}
|
|
result["files"] = files;
|
|
result["afcClient"] =
|
|
QVariant::fromValue(reinterpret_cast<void *>(afcClient));
|
|
result["houseArrestClient"] = QVariant::fromValue(
|
|
reinterpret_cast<void *>(houseArrestClient));
|
|
result["success"] = true;
|
|
|
|
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();
|
|
|
|
// todo
|
|
// 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()) {
|
|
qDebug() << "Error loading app container:"
|
|
<< result.value("error").toString();
|
|
QLabel *errorLabel = new QLabel("No data available for this app");
|
|
errorLabel->setAlignment(Qt::AlignCenter);
|
|
m_containerLayout->addWidget(errorLabel);
|
|
return;
|
|
}
|
|
|
|
// Get the AFC clients from the result and store them as member variables
|
|
m_houseArrestAfcClient = reinterpret_cast<afc_client_t>(
|
|
result.value("afcClient").value<void *>());
|
|
m_houseArrestClient = reinterpret_cast<house_arrest_client_t>(
|
|
result.value("houseArrestClient").value<void *>());
|
|
|
|
if (!m_houseArrestAfcClient) {
|
|
QLabel *errorLabel =
|
|
new QLabel("Failed to get AFC client for app container");
|
|
m_containerLayout->addWidget(errorLabel);
|
|
return;
|
|
}
|
|
|
|
// Create AfcExplorerWidget with the house arrest AFC client
|
|
AfcExplorerWidget *explorer = new AfcExplorerWidget(
|
|
m_device, true, m_houseArrestAfcClient, "/Documents", this);
|
|
explorer->setStyleSheet("border :none;");
|
|
m_containerLayout->addWidget(explorer);
|
|
}
|
|
|
|
void InstalledAppsWidget::onFileSharingFilterChanged(bool enabled)
|
|
{
|
|
Q_UNUSED(enabled)
|
|
// Refresh the apps list when filter changes
|
|
fetchInstalledApps();
|
|
}
|
|
|
|
void InstalledAppsWidget::cleanupHouseArrestClients()
|
|
{
|
|
if (m_houseArrestAfcClient) {
|
|
afc_client_free(m_houseArrestAfcClient);
|
|
m_houseArrestAfcClient = nullptr;
|
|
}
|
|
|
|
if (m_houseArrestClient) {
|
|
house_arrest_client_free(m_houseArrestClient);
|
|
m_houseArrestClient = nullptr;
|
|
}
|
|
}
|
|
|
|
void InstalledAppsWidget::createLeftPanel()
|
|
{
|
|
QWidget *tabWidget = new QWidget();
|
|
tabWidget->setMinimumWidth(100);
|
|
tabWidget->setMaximumWidth(500);
|
|
|
|
QVBoxLayout *tabWidgetLayout = new QVBoxLayout(tabWidget);
|
|
tabWidgetLayout->setContentsMargins(0, 0, 0, 0);
|
|
tabWidgetLayout->setSpacing(0);
|
|
|
|
// Search container
|
|
QWidget *searchContainer = new QWidget();
|
|
searchContainer->setFixedHeight(60);
|
|
QHBoxLayout *searchLayout = new QHBoxLayout(searchContainer);
|
|
searchLayout->setContentsMargins(5, 0, 5, 5);
|
|
|
|
// Search box
|
|
m_searchEdit = new ZLineEdit();
|
|
m_searchEdit->setPlaceholderText("Search apps...");
|
|
searchLayout->addWidget(m_searchEdit);
|
|
|
|
// File sharing filter checkbox
|
|
m_fileSharingCheckBox = new QCheckBox("Show Only File Sharing Enabled");
|
|
m_fileSharingCheckBox->setChecked(true);
|
|
m_fileSharingCheckBox->setStyleSheet("QCheckBox { font-size: 10px; }");
|
|
searchLayout->addWidget(m_fileSharingCheckBox);
|
|
|
|
tabWidgetLayout->addWidget(searchContainer);
|
|
|
|
// App list scroll area
|
|
m_tabScrollArea = new QScrollArea();
|
|
m_tabScrollArea->setWidgetResizable(true);
|
|
m_tabScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
m_tabScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
m_tabScrollArea->setStyleSheet(
|
|
"QScrollArea { background: transparent; border: none; }");
|
|
m_tabScrollArea->viewport()->setStyleSheet("background: transparent;");
|
|
|
|
m_tabContainer = new QWidget();
|
|
m_tabContainer->setStyleSheet("QWidget { background: transparent; }");
|
|
m_tabLayout = new QVBoxLayout(m_tabContainer);
|
|
m_tabLayout->setContentsMargins(0, 0, 10, 0);
|
|
m_tabLayout->setSpacing(10);
|
|
m_tabLayout->addStretch();
|
|
|
|
m_tabScrollArea->setWidget(m_tabContainer);
|
|
tabWidgetLayout->addWidget(m_tabScrollArea);
|
|
|
|
m_splitter->addWidget(tabWidget);
|
|
}
|
|
|
|
void InstalledAppsWidget::createRightPanel()
|
|
{
|
|
QWidget *rightContentWidget = new QWidget();
|
|
|
|
QVBoxLayout *contentLayout = new QVBoxLayout(rightContentWidget);
|
|
contentLayout->setContentsMargins(0, 0, 0, 5);
|
|
contentLayout->setSpacing(0);
|
|
|
|
m_containerWidget = new QWidget();
|
|
m_containerWidget->setObjectName("containerWidget");
|
|
m_containerWidget->setStyleSheet(
|
|
"QWidget#containerWidget { border: none; }");
|
|
m_containerLayout = new QVBoxLayout(m_containerWidget);
|
|
m_containerLayout->setContentsMargins(0, 0, 0, 0);
|
|
m_containerLayout->setSpacing(0);
|
|
|
|
contentLayout->addWidget(m_containerWidget);
|
|
|
|
m_splitter->addWidget(rightContentWidget);
|
|
}
|