Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ set(SOURCES
sources/apksignworker.cpp
sources/appearancesettingswidget.cpp
sources/binarysettingswidget.cpp
sources/desktopdatabaseupdateworker.cpp
sources/devicelistworker.cpp
sources/deviceselectiondialog.cpp
sources/findreplacedialog.cpp
Expand Down Expand Up @@ -101,6 +102,7 @@ set(HEADERS
sources/apksignworker.h
sources/appearancesettingswidget.h
sources/binarysettingswidget.h
sources/desktopdatabaseupdateworker.h
sources/devicelistworker.h
sources/deviceselectiondialog.h
sources/findreplacedialog.h
Expand Down
47 changes: 47 additions & 0 deletions sources/desktopdatabaseupdateworker.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#include <QDebug>
#include <QObject>
#include <QProcess>
#include "desktopdatabaseupdateworker.h"

DesktopDatabaseUpdateWorker::DesktopDatabaseUpdateWorker(const QString &applicationsDir, QObject *parent)
: QObject(parent), m_ApplicationsDir(applicationsDir)
{
}

void DesktopDatabaseUpdateWorker::updateDatabase()
{
emit started();

// Check if update-desktop-database exists
QProcess checkProcess;
checkProcess.start("which", QStringList() << "update-desktop-database");
if (!checkProcess.waitForFinished(1000)) {
// Command doesn't exist or timed out, just finish silently
emit finished();
return;
}

if (checkProcess.exitCode() != 0) {
// update-desktop-database not found, finish silently
emit finished();
return;
}

// Run update-desktop-database
QProcess updateProcess;
updateProcess.start("update-desktop-database", QStringList() << m_ApplicationsDir);

if (!updateProcess.waitForFinished(10000)) { // Wait max 10 seconds
emit error(QObject::tr("Desktop database update timed out"));
emit finished();
return;
}

if (updateProcess.exitCode() != 0) {
QString errorOutput = QString::fromUtf8(updateProcess.readAllStandardError());
emit error(QObject::tr("Desktop database update failed: %1").arg(errorOutput));
}

emit finished();
}

22 changes: 22 additions & 0 deletions sources/desktopdatabaseupdateworker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#ifndef DESKTOPDATABASEUPDATEWORKER_H
#define DESKTOPDATABASEUPDATEWORKER_H

#include <QObject>
#include <QString>

class DesktopDatabaseUpdateWorker : public QObject
{
Q_OBJECT
public:
explicit DesktopDatabaseUpdateWorker(const QString &applicationsDir, QObject *parent = nullptr);
void updateDatabase();
signals:
void finished();
void error(const QString &message);
void started();
private:
QString m_ApplicationsDir;
};

#endif // DESKTOPDATABASEUPDATEWORKER_H

2 changes: 1 addition & 1 deletion sources/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ int main(int argc, char *argv[])
}
app.setPalette(palette);
#endif

// Check for APK file path in command-line arguments
QString apkFilePath;
if (argc > 1) {
Expand Down
192 changes: 190 additions & 2 deletions sources/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@
#include <QDockWidget>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QFrame>
#include <QHeaderView>
#include <QInputDialog>
#include <QMenuBar>
#include <QMessageBox>
#include <QPixmap>
#include <QPushButton>
#include <QProcess>
#include <QProcessEnvironment>
#include <QSettings>
#include <QSignalBlocker>
#include <QStandardPaths>
#include <QStatusBar>
#include <QTabWidget>
#include <QTextStream>
#include <QTextDocumentFragment>
#include <QThread>
#include <QTimer>
Expand All @@ -29,6 +34,7 @@
#include "apkdecompileworker.h"
#include "apkrecompileworker.h"
#include "apksignworker.h"
#include "desktopdatabaseupdateworker.h"
#include "deviceselectiondialog.h"
#include "findreplacedialog.h"
#include "hexedit.h"
Expand Down Expand Up @@ -72,7 +78,14 @@ MainWindow::MainWindow(const QMap<QString, QString> &versions, QWidget *parent)
setMenuBar(buildMenuBar());
setMinimumSize(WINDOW_WIDTH, WINDOW_HEIGHT);
setStatusBar(buildStatusBar(versions));
setWindowTitle(tr("APK Studio").append(" - https://vaibhavpandey.com/apkstudio/"));
updateWindowTitle();

#ifdef Q_OS_LINUX
// Set window icon explicitly for Linux window managers
// This ensures the icon appears in the sidebar/dock when the window is running
setWindowIcon(QIcon(":/images/icon.png"));
#endif

connect(QApplication::clipboard(), &QClipboard::dataChanged, this, &MainWindow::handleClipboardDataChanged);
QSettings settings;
if (settings.value("app_maximized").toBool()) {
Expand All @@ -92,7 +105,12 @@ MainWindow::MainWindow(const QMap<QString, QString> &versions, QWidget *parent)
// Ensure main window gets focus instead of search boxes
setFocus();

QTimer::singleShot(100, [=] {
#ifdef Q_OS_LINUX
// Check and install desktop file on Linux (after window is shown)
QTimer::singleShot(500, this, &MainWindow::checkAndInstallDesktopFile);
#endif

QTimer::singleShot(500, [=] {
QSettings settings;
const QStringList files = settings.value("open_files").toStringList();
foreach (const QString &file, files) {
Expand Down Expand Up @@ -1445,6 +1463,7 @@ void MainWindow::openProject(const QString &folder, const bool last)
openFile(manifest);
}
}
updateWindowTitle();
}

void MainWindow::reloadChildren(QTreeWidgetItem *item)
Expand Down Expand Up @@ -1488,6 +1507,175 @@ bool MainWindow::saveTab(int i)
return true;
}

void MainWindow::updateWindowTitle()
{
QString title = tr("APK Studio by VPZ");

// Get the first (most recent) project from the tree
if (m_ProjectsTree->topLevelItemCount() > 0) {
QTreeWidgetItem *firstProject = m_ProjectsTree->topLevelItem(0);
if (firstProject) {
QString projectFolder = firstProject->data(0, Qt::UserRole + 2).toString();
if (!projectFolder.isEmpty()) {
QFileInfo info(projectFolder);
title += tr(" - %1").arg(info.fileName());
}
}
}

setWindowTitle(title);
}

#ifdef Q_OS_LINUX
void MainWindow::checkAndInstallDesktopFile()
{
QString applicationsDir = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
if (applicationsDir.isEmpty()) {
// Fallback to ~/.local/share/applications
applicationsDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/.local/share/applications";
}

QString desktopFilePath = applicationsDir + "/apkstudio.desktop";
QFileInfo desktopFileInfo(desktopFilePath);

// Check if .desktop file already exists
if (desktopFileInfo.exists()) {
return; // Already installed
}

// Ask user for confirmation
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Install desktop entry"));
msgBox.setText(tr("Would you like to install a desktop entry for APK Studio?\n\nThis will allow you to launch APK Studio from your application menu."));
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
msgBox.setDefaultButton(QMessageBox::Yes);
msgBox.setIcon(QMessageBox::Question);

if (msgBox.exec() != QMessageBox::Yes) {
return; // User declined
}

// Get the executable path
// Check if running from AppImage - if so, use the AppImage file path
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
QString executablePath;
QString appImagePath = env.value("APPIMAGE");

if (!appImagePath.isEmpty() && QFile::exists(appImagePath)) {
// Running from AppImage - use the AppImage file itself
executablePath = appImagePath;
} else {
// Not running from AppImage - use the regular executable path
executablePath = QApplication::applicationFilePath();
QFileInfo exeInfo(executablePath);
if (!exeInfo.exists()) {
// If the executable path doesn't exist (e.g., running from build directory),
// try to use argv[0] or a generic command
executablePath = "apkstudio";
} else {
executablePath = exeInfo.absoluteFilePath();
}
}

// Extract icon from bundled resources and save it to a location the desktop file can use
QString iconPath = "apkstudio"; // Fallback to generic name

// Try to extract icon from bundled resources
QPixmap iconPixmap(":/images/icon.png");
if (!iconPixmap.isNull()) {
// Save icon to ~/.local/share/icons/apkstudio.png
QString iconsDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/.local/share/icons";
QDir iconsDirObj;
if (iconsDirObj.mkpath(iconsDir)) {
QString iconFilePath = iconsDir + "/apkstudio.png";
if (iconPixmap.save(iconFilePath, "PNG")) {
iconPath = iconFilePath;
} else {
// Fallback: try ~/.local/share/apkstudio/icon.png
QString appDataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
if (appDataDir.isEmpty()) {
appDataDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/.apkstudio";
}
if (iconsDirObj.mkpath(appDataDir)) {
iconFilePath = appDataDir + "/icon.png";
if (iconPixmap.save(iconFilePath, "PNG")) {
iconPath = iconFilePath;
}
}
}
}
}

// If icon extraction failed, try to find it relative to executable (for non-AppImage installations)
QFileInfo exeInfo(executablePath);
if (iconPath == "apkstudio" && exeInfo.exists() && appImagePath.isEmpty()) {
QString exeDir = exeInfo.absolutePath();
QFileInfo iconInfo(exeDir + "/../share/pixmaps/apkstudio.png");
if (iconInfo.exists()) {
iconPath = iconInfo.absoluteFilePath();
} else {
// Try relative to executable
iconInfo.setFile(exeDir + "/../share/apkstudio/icon.png");
if (iconInfo.exists()) {
iconPath = iconInfo.absoluteFilePath();
}
}
}

// Create the applications directory if it doesn't exist
QDir dir;
if (!dir.mkpath(applicationsDir)) {
QMessageBox::warning(this, tr("Error"),
tr("Failed to create applications directory:\n%1").arg(applicationsDir));
return;
}

// Create the .desktop file content
QFile desktopFile(desktopFilePath);
if (!desktopFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(this, tr("Error"),
tr("Failed to create desktop file:\n%1").arg(desktopFilePath));
return;
}

QTextStream out(&desktopFile);
out << "[Desktop Entry]\n";
out << "Type=Application\n";
out << "Name=APK Studio\n";
out << "Name[en]=APK Studio\n";
out << "Comment=Android APK reverse engineering tool\n";
out << "Comment[en]=Android APK reverse engineering tool\n";
// Escape the executable path if it contains spaces
QString escapedExecPath = executablePath;
if (executablePath.contains(" ")) {
escapedExecPath = "\"" + executablePath + "\"";
}
out << "Exec=" << escapedExecPath << " %F\n";
out << "Icon=" << iconPath << "\n";
out << "Terminal=false\n";
out << "Categories=Development;Utility;\n";
out << "StartupNotify=true\n";
out << "StartupWMClass=apkstudio\n";
desktopFile.close();

// Set appropriate permissions (readable by user, group, and others)
desktopFile.setPermissions(QFile::ReadUser | QFile::WriteUser |
QFile::ReadGroup | QFile::ReadOther);

// Update desktop database to make it appear in Ubuntu launcher (async using worker)
QThread *thread = new QThread(this);
DesktopDatabaseUpdateWorker *worker = new DesktopDatabaseUpdateWorker(applicationsDir);
worker->moveToThread(thread);

connect(thread, &QThread::started, worker, &DesktopDatabaseUpdateWorker::updateDatabase);
connect(worker, &DesktopDatabaseUpdateWorker::finished, thread, &QThread::quit);
connect(worker, &DesktopDatabaseUpdateWorker::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
// Note: We don't show error messages for database update failures as they're not critical
thread->start();
}
#endif

MainWindow::~MainWindow()
{
}
4 changes: 4 additions & 0 deletions sources/mainwindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ private slots:
void openProject(const QString &folder, const bool last = false);
void reloadChildren(QTreeWidgetItem *item);
void filterProjectTreeItems(QTreeWidgetItem *item, const QString &filter);
void updateWindowTitle();
#ifdef Q_OS_LINUX
void checkAndInstallDesktopFile();
#endif
private:
bool saveTab(int index);
};
Expand Down
Loading