blob: 0ca5215eb2b85d2c1763fb0537d4b37524a7fae9 [file] [log] [blame]
/* Copyright (C) 2015-2016 The Android Open Source Project
**
** This software is licensed under the terms of the GNU General Public
** License version 2, as published by the Free Software Foundation, and
** may be copied, distributed, and modified under those terms.
**
** 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 General Public License for more details.
*/
#include "android/skin/qt/emulator-qt-window.h"
#include "android/android.h"
#include "android/base/async/ThreadLooper.h"
#include "android/base/files/PathUtils.h"
#include "android/base/memory/LazyInstance.h"
#include "android/base/memory/ScopedPtr.h"
#include "android/base/threads/Async.h"
#include "android/cpu_accelerator.h"
#include "android/crashreport/CrashReporter.h"
#include "android/crashreport/crash-handler.h"
#include "android/emulation/control/user_event_agent.h"
#include "android/emulator-window.h"
#include "android/metrics/metrics_reporter_callbacks.h"
#include "android/opengl/gpuinfo.h"
#include "android/skin/event.h"
#include "android/skin/keycode.h"
#include "android/skin/qt/QtLooper.h"
#include "android/skin/qt/event-serializer.h"
#include "android/skin/qt/extended-pages/common.h"
#include "android/skin/qt/qt-settings.h"
#include "android/skin/qt/winsys-qt.h"
#include "android/ui-emu-agent.h"
#if defined(__APPLE__)
#include "android/skin/qt/mac-native-window.h"
#endif
#define DEBUG 1
#if DEBUG
#include "android/utils/debug.h"
#define D(...) VERBOSE_PRINT(surface, __VA_ARGS__)
#else
#define D(...) ((void)0)
#endif
#include <QCheckBox>
#include <QCursor>
#include <QDesktopWidget>
#include <QFileDialog>
#include <QIcon>
#include <QLabel>
#include <QMouseEvent>
#include <QPainter>
#include <QPixmap>
#include <QProgressBar>
#include <QPushButton>
#include <QScreen>
#include <QScrollBar>
#include <QSemaphore>
#include <QSettings>
#include <QWindow>
#include <QtCore>
#include <string>
#include <vector>
using namespace android::base;
using android::emulation::ApkInstaller;
using android::emulation::FilePusher;
using android::emulation::ScreenCapturer;
using std::string;
using std::vector;
// Make sure it is POD here
static LazyInstance<EmulatorQtWindow::Ptr> sInstance = LAZY_INSTANCE_INIT;
// static
const int EmulatorQtWindow::kPushProgressBarMax = 2000;
const StringView EmulatorQtWindow::kRemoteDownloadsDir = "/sdcard/Download/";
// Gingerbread devices download files to the following directory (note the
// uncapitalized "download").
const StringView EmulatorQtWindow::kRemoteDownloadsDirApi10 =
"/sdcard/download/";
void EmulatorQtWindow::create() {
sInstance.get() = Ptr(new EmulatorQtWindow());
}
EmulatorQtWindow::EmulatorQtWindow(QWidget* parent)
: QFrame(parent),
mLooper(android::qt::createLooper()),
mStartupDialog(this),
mContainer(this),
mOverlay(this, &mContainer),
mZoomFactor(1.0),
mInZoomMode(false),
mNextIsZoom(false),
mForwardShortcutsToDevice(false),
mPrevMousePosition(0, 0),
mMainLoopThread(nullptr),
mAvdWarningBox(QMessageBox::Information,
tr("Recommended AVD"),
tr("Running an x86 based Android Virtual Device (AVD) is "
"10x faster.<br/>"
"We strongly recommend creating a new AVD."),
QMessageBox::Ok,
this),
mGpuWarningBox(QMessageBox::Information,
tr("GPU Driver Issue"),
tr("Your GPU driver information:\n\n"
"%1\n"
"Some users have experienced emulator stability issues "
"with this driver version. As a result, we're "
"selecting a software renderer. Please check with "
"your manufacturer to see if there is an updated "
"driver available.")
.arg((GpuInfoList::get()->blacklist_status
? QString::fromStdString(
GpuInfoList::get()->dump())
: "")),
QMessageBox::Ok,
this),
mAdbWarningBox(QMessageBox::Warning,
tr("Detected ADB"),
tr(""),
QMessageBox::Ok,
this),
mFirstShowEvent(true),
mEventLogger(new UIEventRecorder<android::base::CircularBuffer>(
&mEventCapturer,
android::base::CircularBuffer<EventRecord>(1000))),
mUserActionsCounter(new android::qt::UserActionsCounter(&mEventCapturer)),
mAdbInterface(android::emulation::AdbInterface::create(mLooper)),
mApkInstaller(mAdbInterface.get()),
mFilePusher(mAdbInterface.get(),
[this](StringView filePath, FilePusher::Result result) {
adbPushDone(filePath, result);
},
[this](double progress, bool done) {
adbPushProgress(progress, done);
}),
mScreenCapturer(mAdbInterface.get()),
mInstallDialog(this),
mPushDialog(this),
mStartedAdbStopProcess(false) {
android::base::ThreadLooper::setLooper(mLooper, true);
// Start a timer. If the main window doesn't
// appear before the timer expires, show a
// pop-up to let the user know we're still
// working.
QObject::connect(&mStartupTimer, &QTimer::timeout, this,
&EmulatorQtWindow::slot_startupTick);
mStartupTimer.setSingleShot(true);
mStartupTimer.setInterval(500); // Half a second
mStartupTimer.start();
mBackingSurface = NULL;
mToolWindow = new ToolWindow(this, &mContainer, mEventLogger,
mUserActionsCounter);
this->setAcceptDrops(true);
QObject::connect(this, &EmulatorQtWindow::blit, this,
&EmulatorQtWindow::slot_blit);
QObject::connect(this, &EmulatorQtWindow::createBitmap, this,
&EmulatorQtWindow::slot_createBitmap);
QObject::connect(this, &EmulatorQtWindow::fill, this,
&EmulatorQtWindow::slot_fill);
QObject::connect(this, &EmulatorQtWindow::getBitmapInfo, this,
&EmulatorQtWindow::slot_getBitmapInfo);
QObject::connect(this, &EmulatorQtWindow::getDevicePixelRatio, this,
&EmulatorQtWindow::slot_getDevicePixelRatio);
QObject::connect(this, &EmulatorQtWindow::getScreenDimensions, this,
&EmulatorQtWindow::slot_getScreenDimensions);
QObject::connect(this, &EmulatorQtWindow::getWindowId, this,
&EmulatorQtWindow::slot_getWindowId);
QObject::connect(this, &EmulatorQtWindow::getWindowPos, this,
&EmulatorQtWindow::slot_getWindowPos);
QObject::connect(this, &EmulatorQtWindow::isWindowFullyVisible, this,
&EmulatorQtWindow::slot_isWindowFullyVisible);
QObject::connect(this, &EmulatorQtWindow::pollEvent, this,
&EmulatorQtWindow::slot_pollEvent);
QObject::connect(this, &EmulatorQtWindow::releaseBitmap, this,
&EmulatorQtWindow::slot_releaseBitmap);
QObject::connect(this, &EmulatorQtWindow::requestClose, this,
&EmulatorQtWindow::slot_requestClose);
QObject::connect(this, &EmulatorQtWindow::requestUpdate, this,
&EmulatorQtWindow::slot_requestUpdate);
QObject::connect(this, &EmulatorQtWindow::setDeviceGeometry, this,
&EmulatorQtWindow::slot_setDeviceGeometry);
QObject::connect(this, &EmulatorQtWindow::setWindowIcon, this,
&EmulatorQtWindow::slot_setWindowIcon);
QObject::connect(this, &EmulatorQtWindow::setWindowPos, this,
&EmulatorQtWindow::slot_setWindowPos);
QObject::connect(this, &EmulatorQtWindow::setTitle, this,
&EmulatorQtWindow::slot_setWindowTitle);
QObject::connect(this, &EmulatorQtWindow::showWindow, this,
&EmulatorQtWindow::slot_showWindow);
QObject::connect(this, &EmulatorQtWindow::runOnUiThread, this,
&EmulatorQtWindow::slot_runOnUiThread);
QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit,
this, &EmulatorQtWindow::slot_clearInstance);
// TODO: make dialogs affected by themes and changes
mInstallDialog.setWindowTitle(tr("APK Installer"));
mInstallDialog.setLabelText(tr("Installing APK..."));
mInstallDialog.setRange(0, 0); // Makes it a "busy" dialog
mInstallDialog.setModal(true);
mInstallDialog.close();
QObject::connect(&mInstallDialog, SIGNAL(canceled()), this,
SLOT(slot_installCanceled()));
mPushDialog.setWindowTitle(tr("File Copy"));
mPushDialog.setLabelText(tr("Copying files..."));
mPushDialog.setRange(0, kPushProgressBarMax);
mPushDialog.close();
QObject::connect(&mPushDialog, SIGNAL(canceled()), this,
SLOT(slot_adbPushCanceled()));
QObject::connect(mContainer.horizontalScrollBar(),
SIGNAL(valueChanged(int)), this,
SLOT(slot_horizontalScrollChanged(int)));
QObject::connect(mContainer.verticalScrollBar(), SIGNAL(valueChanged(int)),
this, SLOT(slot_verticalScrollChanged(int)));
QObject::connect(mContainer.horizontalScrollBar(),
SIGNAL(rangeChanged(int, int)), this,
SLOT(slot_scrollRangeChanged(int, int)));
QObject::connect(mContainer.verticalScrollBar(),
SIGNAL(rangeChanged(int, int)), this,
SLOT(slot_scrollRangeChanged(int, int)));
QSettings settings;
bool onTop = settings.value(Ui::Settings::ALWAYS_ON_TOP, false).toBool();
setOnTop(onTop);
bool shortcutBool =
settings.value(Ui::Settings::FORWARD_SHORTCUTS_TO_DEVICE, false)
.toBool();
setForwardShortcutsToDevice(shortcutBool ? 1 : 0);
initErrorDialog(this);
setObjectName("MainWindow");
mEventLogger->startRecording(this);
mEventLogger->startRecording(mToolWindow);
mUserActionsCounter->startCountingForMainWindow(this);
mUserActionsCounter->startCountingForToolWindow(mToolWindow);
mUserActionsCounter->startCountingForOverlayWindow(&mOverlay);
// The crash reporter will dump the last 1000 UI events.
// mEventLogger is a shared pointer, capturing its copy
// inside a lambda ensures that it lives on as long as
// CrashReporter needs it, even if EmulatorQtWindow is
// destroyed.
auto event_logger = mEventLogger;
android::crashreport::CrashReporter::get()->addCrashCallback(
[event_logger]() {
android::crashreport::CrashReporter::get()->attachData(
"recent-ui-actions.txt",
serializeEvents(event_logger->container()));
});
auto user_actions = mUserActionsCounter;
android::crashreport::CrashReporter::get()->addCrashCallback(
[user_actions]() {
android::crashreport::CrashReporter::get()->attachData(
"num-user-actions.txt",
std::to_string(user_actions->count()));
});
std::weak_ptr<android::qt::UserActionsCounter> user_actions_weak(
mUserActionsCounter);
android::metrics::addTickCallback([user_actions_weak](AndroidMetrics* am) {
if (auto user_actions = user_actions_weak.lock()) {
am->user_actions = user_actions->count();
}
});
mWheelScrollTimer.setInterval(100);
mWheelScrollTimer.setSingleShot(true);
connect(&mWheelScrollTimer, SIGNAL(timeout()), this,
SLOT(wheelScrollTimeout()));
}
EmulatorQtWindow::Ptr EmulatorQtWindow::getInstancePtr() {
return sInstance.get();
}
EmulatorQtWindow* EmulatorQtWindow::getInstance() {
return getInstancePtr().get();
}
EmulatorQtWindow::~EmulatorQtWindow() {
if (mApkInstallCommand) {
mApkInstallCommand->cancel();
}
mInstallDialog.disconnect();
mInstallDialog.close();
mPushDialog.disconnect();
mPushDialog.close();
deleteErrorDialog();
if (mToolWindow) {
delete mToolWindow;
mToolWindow = NULL;
}
delete mMainLoopThread;
}
void EmulatorQtWindow::showAvdArchWarning() {
ScopedCPtr<char> arch(avdInfo_getTargetCpuArch(android_avdInfo));
if (!strcmp(arch.get(), "x86") || !strcmp(arch.get(), "x86_64")) {
return;
}
// The following statuses indicate that the machine hardware does not
// support
// hardware
// acceleration. These machines should never show a popup indicating to
// switch
// to x86.
static const AndroidCpuAcceleration badStatuses[] = {
ANDROID_CPU_ACCELERATION_NESTED_NOT_SUPPORTED, // HAXM doesn't
// support
// nested VM
ANDROID_CPU_ACCELERATION_INTEL_REQUIRED, // HAXM requires
// GeniuneIntel
// processor
ANDROID_CPU_ACCELERATION_NO_CPU_SUPPORT, // CPU doesn't support
// required
// features (VT-x or SVM)
ANDROID_CPU_ACCELERATION_NO_CPU_VTX_SUPPORT, // CPU doesn't support
// VT-x
ANDROID_CPU_ACCELERATION_NO_CPU_NX_SUPPORT, // CPU doesn't support
// NX
};
AndroidCpuAcceleration cpuStatus =
androidCpuAcceleration_getStatus(nullptr);
for (AndroidCpuAcceleration status : badStatuses) {
if (cpuStatus == status) {
return;
}
}
QSettings settings;
if (settings.value(Ui::Settings::SHOW_AVD_ARCH_WARNING, true).toBool()) {
QObject::connect(&mAvdWarningBox,
SIGNAL(buttonClicked(QAbstractButton*)), this,
SLOT(slot_avdArchWarningMessageAccepted()));
QCheckBox* checkbox = new QCheckBox(tr("Never show this again."));
checkbox->setCheckState(Qt::Unchecked);
mAvdWarningBox.setWindowModality(Qt::NonModal);
mAvdWarningBox.setCheckBox(checkbox);
mAvdWarningBox.show();
}
}
void EmulatorQtWindow::showGpuWarning() {
if (!GpuInfoList::get()->blacklist_status) {
return;
}
QSettings settings;
if (settings.value(Ui::Settings::SHOW_GPU_WARNING, true).toBool()) {
QObject::connect(&mGpuWarningBox,
SIGNAL(buttonClicked(QAbstractButton*)), this,
SLOT(slot_gpuWarningMessageAccepted()));
QCheckBox* checkbox = new QCheckBox(tr("Never show this again."));
checkbox->setCheckState(Qt::Unchecked);
mGpuWarningBox.setWindowModality(Qt::NonModal);
mGpuWarningBox.setCheckBox(checkbox);
mGpuWarningBox.show();
}
}
void EmulatorQtWindow::slot_startupTick() {
// It's been a while since we were launched, and the main
// window still hasn't appeared.
// Show a pop-up that lets the user know we are working.
mStartupDialog.setWindowTitle(tr("Android Emulator"));
// Hide close/minimize/maximize buttons
mStartupDialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint |
Qt::WindowTitleHint);
// Make sure the icon is the same as in the main window
mStartupDialog.setWindowIcon(QApplication::windowIcon());
// Emulator logo
QLabel* label = new QLabel();
label->setAlignment(Qt::AlignCenter);
QSize size;
size.setWidth(mStartupDialog.size().width() / 2);
size.setHeight(size.width());
QPixmap pixmap = windowIcon().pixmap(size);
label->setPixmap(pixmap);
mStartupDialog.setLabel(label);
// The default progress bar on Windows isn't centered for some reason
QProgressBar* bar = new QProgressBar();
bar->setAlignment(Qt::AlignHCenter);
mStartupDialog.setBar(bar);
mStartupDialog.setRange(0, 0); // Don't show % complete
mStartupDialog.setCancelButton(0); // No "cancel" button
mStartupDialog.show();
}
void EmulatorQtWindow::slot_avdArchWarningMessageAccepted() {
QCheckBox* checkbox = mAvdWarningBox.checkBox();
if (checkbox->checkState() == Qt::Checked) {
QSettings settings;
settings.setValue(Ui::Settings::SHOW_AVD_ARCH_WARNING, false);
}
}
void EmulatorQtWindow::slot_gpuWarningMessageAccepted() {
QCheckBox* checkbox = mGpuWarningBox.checkBox();
if (checkbox->checkState() == Qt::Checked) {
QSettings settings;
settings.setValue(Ui::Settings::SHOW_GPU_WARNING, false);
}
}
void EmulatorQtWindow::closeEvent(QCloseEvent* event) {
crashhandler_exitmode(__FUNCTION__);
if (mMainLoopThread && mMainLoopThread->isRunning()) {
// we dont want to restore to a state where the
// framework is stopped by 'adb shell stop'
// so skip that step when saving vm on exit
if (savevm_on_exit) {
queueQuitEvent();
} else {
runAdbShellStopAndQuit();
}
event->ignore();
} else {
event->accept();
}
}
void EmulatorQtWindow::queueQuitEvent() {
queueSkinEvent(createSkinEvent(kEventQuit));
}
void EmulatorQtWindow::dragEnterEvent(QDragEnterEvent* event) {
// Accept all drag enter events with any URL, then filter more in drop
// events
// TODO: check this with hasFormats() using MIME type for .apk?
if (event->mimeData() && event->mimeData()->hasUrls()) {
event->acceptProposedAction();
}
}
void EmulatorQtWindow::dropEvent(QDropEvent* event) {
// Modal dialogs don't prevent drag-and-drop! Manually check for a modal
// dialog, and if so, reject the event.
if (QApplication::activeModalWidget() != nullptr) {
event->ignore();
return;
}
QList<QUrl> urls = event->mimeData()->urls();
if (urls.length() == 0) {
showErrorDialog(
tr("Dropped content didn't have any files. Emulator only "
"supports drag-and-drop for files to copy them or install "
"a single APK."),
tr("Drag and Drop"));
return;
}
// Get the first url - if it's an APK and the only file, attempt to install
// it
QString url = urls[0].toLocalFile();
if (url.endsWith(".apk") && urls.length() == 1) {
runAdbInstall(url);
return;
} else {
// If any of the files is an APK, intent was ambiguous
for (int i = 0; i < urls.length(); i++) {
if (urls[i].path().endsWith(".apk")) {
showErrorDialog(
tr("Drag-and-drop can either install a single APK"
" file or copy one or more non-APK files to the"
" Emulator SD card."),
tr("Drag and Drop"));
return;
}
}
runAdbPush(urls);
}
}
void EmulatorQtWindow::keyPressEvent(QKeyEvent* event) {
handleKeyEvent(kEventKeyDown, event);
}
void EmulatorQtWindow::keyReleaseEvent(QKeyEvent* event) {
handleKeyEvent(kEventKeyUp, event);
// If we enabled trackball mode, tell Qt to always forward mouse movement
// events. Otherwise, Qt will forward them only when a button is pressed.
EmulatorWindow* ew = emulator_window_get();
bool trackballActive = skin_ui_is_trackball_active(ew->ui);
if (trackballActive != hasMouseTracking()) {
setMouseTracking(trackballActive);
}
}
void EmulatorQtWindow::mouseMoveEvent(QMouseEvent* event) {
handleMouseEvent(kEventMouseMotion, getSkinMouseButton(event),
event->pos());
}
void EmulatorQtWindow::mousePressEvent(QMouseEvent* event) {
handleMouseEvent(kEventMouseButtonDown, getSkinMouseButton(event),
event->pos());
}
void EmulatorQtWindow::mouseReleaseEvent(QMouseEvent* event) {
handleMouseEvent(kEventMouseButtonUp, getSkinMouseButton(event),
event->pos());
}
void EmulatorQtWindow::paintEvent(QPaintEvent*) {
QPainter painter(this);
QRect bg(QPoint(0, 0), this->size());
painter.fillRect(bg, Qt::black);
// Ensure we actually have a valid bitmap before attempting to
// rescale
if (mBackingSurface && !mBackingSurface->bitmap->isNull()) {
QRect r(0, 0, mBackingSurface->w, mBackingSurface->h);
// Rescale with smooth transformation to avoid aliasing
QImage scaled_bitmap = mBackingSurface->bitmap->scaled(
r.size() * devicePixelRatio(), Qt::KeepAspectRatio,
Qt::SmoothTransformation);
if (!scaled_bitmap.isNull()) {
scaled_bitmap.setDevicePixelRatio(devicePixelRatio());
painter.drawImage(r, scaled_bitmap);
} else {
qWarning("Failed to scale the skin bitmap");
}
} else {
D("Painting emulator window, but no backing bitmap");
}
}
void EmulatorQtWindow::raise() {
mContainer.raise();
mToolWindow->raise();
}
void EmulatorQtWindow::show() {
mContainer.show();
QFrame::show();
mToolWindow->show();
QObject::connect(window()->windowHandle(), &QWindow::screenChanged, this,
&EmulatorQtWindow::slot_screenChanged);
// On Mac, the above function won't be triggered when you plug in a new
// monitor and the OS move the emulator to the new screen. In such
// situation, it will trigger screenCountChanged.
QObject::connect(qApp->desktop(), &QDesktopWidget::screenCountChanged, this,
&EmulatorQtWindow::slot_screenChanged);
// There is still a corner case where user can miss the screen change event
// by changing the primary display through system setting. This can be
// captured by the function below, but it won't be supported until Qt 5.6.
//
// TODO(yahan): uncomment the following line when upgrade to Qt 5.6
// QObject::connect(qApp->desktop(), &QDesktopWidget::primaryScreenChanged,
// this, &EmulatorQtWindow::slot_screenChanged);
}
void EmulatorQtWindow::setOnTop(bool onTop) {
setFrameOnTop(&mContainer, onTop);
setFrameOnTop(mToolWindow, onTop);
}
void EmulatorQtWindow::showMinimized() {
mContainer.showMinimized();
}
void EmulatorQtWindow::startThread(StartFunction f, int argc, char** argv) {
if (!mMainLoopThread) {
// pass the QEMU main thread's arguments into the crash handler
std::string arguments = "===== QEMU main loop arguments =====\n";
for (int i = 0; i < argc; ++i) {
arguments += argv[i];
arguments += '\n';
}
android::crashreport::CrashReporter::get()->attachData(
"qemu-main-loop-args.txt", arguments);
mMainLoopThread = new MainLoopThread(f, argc, argv);
QObject::connect(mMainLoopThread, &QThread::finished, &mContainer,
&EmulatorContainer::close);
mMainLoopThread->start();
} else {
D("mMainLoopThread already started");
}
}
void EmulatorQtWindow::slot_blit(QImage* src,
QRect* srcRect,
QImage* dst,
QPoint* dstPos,
QPainter::CompositionMode* op,
QSemaphore* semaphore) {
QPainter painter(dst);
painter.setCompositionMode(*op);
painter.drawImage(*dstPos, *src, *srcRect);
painter.setCompositionMode(QPainter::CompositionMode_Source);
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_clearInstance() {
#ifndef __APPLE__
if (mToolWindow) {
delete mToolWindow;
mToolWindow = NULL;
}
#endif
skin_winsys_save_window_pos();
sInstance.get().reset();
}
void EmulatorQtWindow::slot_createBitmap(SkinSurface* s,
int w,
int h,
QSemaphore* semaphore) {
s->bitmap = new QImage(w, h, QImage::Format_ARGB32);
if (s->bitmap->isNull()) {
// Failed to create image, warn user.
showErrorDialog(tr("Failed to allocate memory for the skin bitmap."
"Try configuring your AVD to not have a skin."),
tr("Error displaying skin"));
} else {
s->bitmap->fill(0);
}
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_fill(SkinSurface* s,
const QRect* rect,
const QColor* color,
QSemaphore* semaphore) {
QPainter painter(s->bitmap);
painter.fillRect(*rect, *color);
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_getBitmapInfo(SkinSurface* s,
SkinSurfacePixels* pix,
QSemaphore* semaphore) {
pix->pixels = (uint32_t*)s->bitmap->bits();
pix->w = s->original_w;
pix->h = s->original_h;
pix->pitch = s->bitmap->bytesPerLine();
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_getDevicePixelRatio(double* out_dpr,
QSemaphore* semaphore) {
*out_dpr = devicePixelRatio();
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_getScreenDimensions(QRect* out_rect,
QSemaphore* semaphore) {
QRect rect = ((QApplication*)QApplication::instance())
->desktop()
->screenGeometry();
out_rect->setX(rect.x());
out_rect->setY(rect.y());
// Always report slightly smaller-than-actual dimensions to prevent odd
// resizing behavior,
// which can happen if things like the OSX dock are not taken into account.
// The difference
// below is specifically to take into account the OSX dock.
out_rect->setWidth(rect.width() * .95);
#ifdef __APPLE__
out_rect->setHeight(rect.height() * .85);
#else // _WIN32 || __linux__
out_rect->setHeight(rect.height() * .95);
#endif
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_getWindowId(WId* out_id, QSemaphore* semaphore) {
WId wid = effectiveWinId();
D("Effective win ID is %lx", wid);
#if defined(__APPLE__)
wid = (WId)getNSWindow((void*)wid);
D("After finding parent, win ID is %lx", wid);
#endif
*out_id = wid;
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_getWindowPos(int* xx,
int* yy,
QSemaphore* semaphore) {
// Note that mContainer.x() == mContainer.frameGeometry().x(), which
// is NOT what we want.
QRect geom = mContainer.geometry();
*xx = geom.x();
*yy = geom.y();
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_isWindowFullyVisible(bool* out_value,
QSemaphore* semaphore) {
QDesktopWidget* desktop =
((QApplication*)QApplication::instance())->desktop();
int screenNum =
desktop->screenNumber(&mContainer); // Screen holding the app
QRect screenGeo = desktop->screenGeometry(screenNum);
*out_value = screenGeo.contains(mContainer.geometry());
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_pollEvent(SkinEvent* event,
bool* hasEvent,
QSemaphore* semaphore) {
if (mSkinEventQueue.isEmpty()) {
*hasEvent = false;
} else {
*hasEvent = true;
SkinEvent* newEvent = mSkinEventQueue.dequeue();
// TODO(grigoryj): debug output needed for investigating the rotation
// bug.
if (VERBOSE_CHECK(rotation) && (newEvent->type == kEventLayoutNext ||
newEvent->type == kEventLayoutPrev)) {
qWarning("Dequed event Layout%s",
newEvent->type == kEventLayoutNext ? "Next" : "Prev");
}
memcpy(event, newEvent, sizeof(SkinEvent));
delete newEvent;
}
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::queueSkinEvent(SkinEvent* event) {
const bool firstEvent = mSkinEventQueue.isEmpty();
// For the following two events, only the "last" example of said event
// matters, so ensure
// that there is only one of them in the queue at a time.
bool replaced = false;
if (event->type == kEventScrollBarChanged ||
event->type == kEventZoomedWindowResized) {
for (int i = 0; i < mSkinEventQueue.size(); i++) {
if (mSkinEventQueue.at(i)->type == event->type) {
SkinEvent* toDelete = mSkinEventQueue.at(i);
mSkinEventQueue.replace(i, event);
delete toDelete;
replaced = true;
break;
}
}
}
if (!replaced) {
mSkinEventQueue.enqueue(event);
// TODO(grigoryj): debug output needed for investigating the
// rotation bug.
if (VERBOSE_CHECK(rotation) && (event->type == kEventLayoutNext ||
event->type == kEventLayoutPrev)) {
qWarning("Enqueued Layout%s event",
event->type == kEventLayoutNext ? "Next" : "Prev");
}
}
const auto uiAgent = mToolWindow->getUiEmuAgent();
if (firstEvent && uiAgent && uiAgent->userEvents &&
uiAgent->userEvents->onNewUserEvent) {
// we know that as soon as emulator starts processing user events
// it processes them until there are none. So we can notify it only
// if this event is the first one
uiAgent->userEvents->onNewUserEvent();
}
if (event->type == kEventLayoutNext) {
emit(layoutChanged(true));
} else if (event->type == kEventLayoutPrev) {
emit(layoutChanged(false));
}
}
void EmulatorQtWindow::slot_releaseBitmap(SkinSurface* s,
QSemaphore* semaphore) {
if (mBackingSurface == s) {
mBackingSurface = NULL;
}
delete s->bitmap;
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_requestClose(QSemaphore* semaphore) {
crashhandler_exitmode(__FUNCTION__);
mContainer.close();
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_requestUpdate(const QRect* rect,
QSemaphore* semaphore) {
if (!mBackingSurface) return;
QRect r(rect->x() * mBackingSurface->w / mBackingSurface->original_w,
rect->y() * mBackingSurface->h / mBackingSurface->original_h,
rect->width() * mBackingSurface->w / mBackingSurface->original_w,
rect->height() * mBackingSurface->h / mBackingSurface->original_h);
update(r);
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_setDeviceGeometry(const QRect* rect,
QSemaphore* semaphore) {
mDeviceGeometry =
QRect(rect->x(), rect->y(), rect->width(), rect->height());
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_setWindowPos(int x, int y, QSemaphore* semaphore) {
mContainer.move(x, y);
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_setWindowIcon(const unsigned char* data,
int size,
QSemaphore* semaphore) {
QPixmap image;
image.loadFromData(data, size);
QIcon icon(image);
QApplication::setWindowIcon(icon);
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_setWindowTitle(const QString* title,
QSemaphore* semaphore) {
mContainer.setWindowTitle(*title);
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_showWindow(SkinSurface* surface,
const QRect* rect,
QSemaphore* semaphore) {
mBackingSurface = surface;
showNormal();
setFixedSize(rect->size());
// If this was the result of a zoom, don't change the overall window size,
// and adjust the
// scroll bars to reflect the desired focus point.
if (mInZoomMode && mNextIsZoom) {
mContainer.stopResizeTimer();
recenterFocusPoint();
} else if (!mNextIsZoom) {
mContainer.resize(rect->size());
}
mNextIsZoom = false;
show();
// Zooming forces the scroll bar to be visible for sizing purposes. They
// should never be shown when not in zoom mode, and should only show when
// necessary when in zoom mode.
if (mInZoomMode) {
mContainer.setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
mContainer.setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
} else {
mContainer.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
mContainer.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
}
// If the user isn't using an x86 AVD, make sure its because their machine
// doesn't support
// CPU acceleration. If it does, recommend switching to an x86 AVD.
// This cannot be done on the construction of the window since the Qt UI
// thread has not been
// properly initialized yet.
if (mFirstShowEvent) {
showAvdArchWarning();
showGpuWarning();
checkAdbVersionAndWarn();
}
mFirstShowEvent = false;
if (semaphore != NULL)
semaphore->release();
}
void EmulatorQtWindow::slot_screenChanged() {
queueSkinEvent(createSkinEvent(kEventScreenChanged));
}
void EmulatorQtWindow::slot_horizontalScrollChanged(int value) {
simulateScrollBarChanged(value, mContainer.verticalScrollBar()->value());
}
void EmulatorQtWindow::slot_verticalScrollChanged(int value) {
simulateScrollBarChanged(mContainer.horizontalScrollBar()->value(), value);
}
void EmulatorQtWindow::slot_scrollRangeChanged(int min, int max) {
simulateScrollBarChanged(mContainer.horizontalScrollBar()->value(),
mContainer.verticalScrollBar()->value());
}
void EmulatorQtWindow::screenshot() {
static const int MIN_SCREENSHOT_API = 14;
if (avdInfo_getApiLevel(android_avdInfo) < MIN_SCREENSHOT_API) {
showErrorDialog(tr("Screenshot is not supported below API 14."),
tr("Screenshot"));
return;
}
auto args = getAdbFullPathStd();
if (args.empty()) {
showErrorDialog(
tr("Could not locate 'adb'<br/>"
"Check settings to verify that your chosen adb path is "
"valid."),
tr("Screenshot"));
return;
}
QString savePath = getScreenshotSaveDirectory();
if (savePath.isEmpty()) {
showErrorDialog(tr("The screenshot save location is invalid.<br/>"
"Check the settings page and ensure the directory "
"exists and is writeable."),
tr("Screenshot"));
return;
}
mScreenCapturer.capture(
savePath.toStdString(),
[this](ScreenCapturer::Result result) {
EmulatorQtWindow::screenshotDone(result);
});
// Display the flash animation immediately as feedback - if it fails, an
// error dialog will indicate as such.
mOverlay.showAsFlash();
}
void EmulatorQtWindow::screenshotDone(ScreenCapturer::Result result) {
QString msg;
switch (result) {
case ScreenCapturer::Result::kSuccess:
return;
case ScreenCapturer::Result::kOperationInProgress:
msg =
tr("Another screen capture is already in progress.<br/>"
"Please try again later.");
break;
case ScreenCapturer::Result::kCaptureFailed:
msg =
tr("The screenshot could not be captured.<br/>"
"Check settings to verify that your chosen adb path is "
"valid.");
break;
case ScreenCapturer::Result::kSaveLocationInvalid:
msg =
tr("The screenshot save location is invalid.<br/>"
"Check the settings page and ensure the directory "
"exists and is writeable.");
break;
case ScreenCapturer::Result::kPullFailed:
msg = tr("The screenshot could not be loaded from the device.");
break;
default:
msg =
tr("There was an unknown error while capturing the "
"screenshot.");
}
showErrorDialog(msg, tr("Screenshot"));
}
void EmulatorQtWindow::slot_installCanceled() {
if (mApkInstallCommand && mApkInstallCommand->inFlight()) {
mApkInstallCommand->cancel();
}
}
void EmulatorQtWindow::runAdbInstall(const QString& path) {
if (mApkInstallCommand && mApkInstallCommand->inFlight()) {
// Modal dialogs should prevent this.
return;
}
// Show a dialog so the user knows something is happening
mInstallDialog.show();
mApkInstallCommand = mApkInstaller.install(
path.toStdString(),
[this](ApkInstaller::Result result, StringView errorString) {
installDone(result, errorString);
});
}
void EmulatorQtWindow::installDone(ApkInstaller::Result result,
StringView errorString) {
mInstallDialog.hide();
QString msg;
switch (result) {
case ApkInstaller::Result::kSuccess:
return;
case ApkInstaller::Result::kOperationInProgress:
msg =
tr("Another APK install is already in progress.<br/>"
"Please try again after it completes.");
break;
case ApkInstaller::Result::kApkPermissionsError:
msg =
tr("Unable to read the given APK.<br/>"
"Ensure that the file is readable.");
break;
case ApkInstaller::Result::kAdbConnectionFailed:
msg =
tr("Failed to start adb.<br/>"
"Check settings to verify your chosen adb path is "
"valid.");
break;
case ApkInstaller::Result::kInstallFailed:
msg = tr("The APK failed to install.<br/> Error: %1")
.arg(errorString.c_str());
break;
default:
msg = tr("There was an unknown error while installing the APK.");
}
showErrorDialog(msg, tr("APK Installer"));
}
void EmulatorQtWindow::runAdbPush(const QList<QUrl>& urls) {
auto adbCommandArgs = getAdbFullPathStd();
if (adbCommandArgs.empty()) {
showErrorDialog(
tr("Could not locate 'adb'<br/>"
"Check settings to verify that your chosen adb path is "
"valid."),
tr("File Copy"));
return;
}
std::vector<std::pair<std::string, std::string>> file_paths;
StringView remoteDownloadsDir = avdInfo_getApiLevel(android_avdInfo) > 10
? kRemoteDownloadsDir
: kRemoteDownloadsDirApi10;
for (const auto& url : urls) {
string remoteFile = PathUtils::join(remoteDownloadsDir,
url.fileName().toStdString());
file_paths.push_back(
std::make_pair(url.toLocalFile().toStdString(), remoteFile));
}
mFilePusher.pushFiles(
file_paths.begin(),
file_paths.end());
}
void EmulatorQtWindow::slot_adbPushCanceled() {
mFilePusher.cancel();
}
void EmulatorQtWindow::adbPushProgress(double progress, bool done) {
if (done) {
mPushDialog.hide();
return;
}
mPushDialog.setValue(progress * kPushProgressBarMax);
mPushDialog.show();
}
void EmulatorQtWindow::adbPushDone(StringView filePath,
FilePusher::Result result) {
QString msg;
switch (result) {
case FilePusher::Result::Success:
return;
case FilePusher::Result::ProcessStartFailure:
msg = tr("Could not launch process to copy %1")
.arg(filePath.c_str());
break;
case FilePusher::Result::FileReadError:
msg = tr("Could not locate %1").arg(filePath.c_str());
break;
case FilePusher::Result::AdbPushFailure:
msg = tr("'adb push' failed for %1").arg(filePath.c_str());
break;
default:
msg = tr("Could not copy %1").arg(filePath.c_str());
}
showErrorDialog(msg, tr("File Copy"));
}
vector<string> EmulatorQtWindow::getAdbFullPathStd() {
std::string adbPath;
QSettings settings;
if (settings.value(Ui::Settings::AUTO_FIND_ADB, true).toBool()) {
if (!mAdbInterface->detectedAdbPath().empty()) {
adbPath = mAdbInterface->detectedAdbPath();
} else {
showErrorDialog(tr("Could not automatically find ADB.<br>"
"Please use the settings page to manually set "
"an ADB path."),
tr("ADB"));
return {};
}
} else {
adbPath = settings.value(Ui::Settings::ADB_PATH, "")
.toString()
.toStdString();
}
vector<string> args{
adbPath, "-s",
android::base::StringFormat("emulator-%d", android_base_port)};
return args;
}
// Convert a Qt::Key_XXX code into the corresponding Linux keycode value.
// On failure, return -1.
static int convertKeyCode(int sym) {
#define KK(x, y) \
{ Qt::Key_##x, KEY_##y }
#define K1(x) KK(x, x)
static const struct {
int qt_sym;
int keycode;
} kConvert[] = {
KK(Left, LEFT),
KK(Right, RIGHT),
KK(Up, UP),
KK(Down, DOWN),
K1(0),
K1(1),
K1(2),
K1(3),
K1(4),
K1(5),
K1(6),
K1(7),
K1(8),
K1(9),
K1(F1),
K1(F2),
K1(F3),
K1(F4),
K1(F5),
K1(F6),
K1(F7),
K1(F8),
K1(F9),
K1(F10),
K1(F11),
K1(F12),
K1(A),
K1(B),
K1(C),
K1(D),
K1(E),
K1(F),
K1(G),
K1(H),
K1(I),
K1(J),
K1(K),
K1(L),
K1(M),
K1(N),
K1(O),
K1(P),
K1(Q),
K1(R),
K1(S),
K1(T),
K1(U),
K1(V),
K1(W),
K1(X),
K1(Y),
K1(Z),
KK(Minus, MINUS),
KK(Equal, EQUAL),
KK(Backspace, BACKSPACE),
KK(Home, HOME),
KK(Escape, ESC),
KK(Comma, COMMA),
KK(Period, DOT),
KK(Space, SPACE),
KK(Slash, SLASH),
KK(Return, ENTER),
KK(Tab, TAB),
KK(BracketLeft, LEFTBRACE),
KK(BracketRight, RIGHTBRACE),
KK(Backslash, BACKSLASH),
KK(Semicolon, SEMICOLON),
KK(Apostrophe, APOSTROPHE),
};
const size_t kConvertSize = sizeof(kConvert) / sizeof(kConvert[0]);
size_t nn;
for (nn = 0; nn < kConvertSize; ++nn) {
if (sym == kConvert[nn].qt_sym) {
return kConvert[nn].keycode;
}
}
return -1;
}
SkinEvent* EmulatorQtWindow::createSkinEvent(SkinEventType type) {
SkinEvent* skin_event = new SkinEvent();
skin_event->type = type;
return skin_event;
}
void EmulatorQtWindow::doResize(const QSize& size,
bool isKbdShortcut,
bool flipDimensions) {
if (!mBackingSurface) return;
int originalWidth = flipDimensions ? mBackingSurface->original_h
: mBackingSurface->original_w;
int originalHeight = flipDimensions ? mBackingSurface->original_w
: mBackingSurface->original_h;
QSize newSize(originalWidth, originalHeight);
newSize.scale(size, Qt::KeepAspectRatio);
// Make sure the new size is always a little bit smaller than the
// screen to prevent keyboard shortcut scaling from making a window
// too large for the screen, which can result in the showing of the
// scroll bars. This is not an issue when resizing by dragging the
// corner because the OS will prevent too large a window.
if (isKbdShortcut) {
QRect screenDimensions;
slot_getScreenDimensions(&screenDimensions);
if (newSize.width() > screenDimensions.width() ||
newSize.height() > screenDimensions.height()) {
newSize.scale(screenDimensions.size(), Qt::KeepAspectRatio);
}
}
double widthScale = (double)newSize.width() / (double)originalWidth;
double heightScale = (double)newSize.height() / (double)originalHeight;
simulateSetScale(std::max(.2, std::min(widthScale, heightScale)));
}
SkinMouseButtonType EmulatorQtWindow::getSkinMouseButton(
QMouseEvent* event) const {
return (event->button() == Qt::RightButton) ? kMouseButtonRight
: kMouseButtonLeft;
}
void EmulatorQtWindow::handleMouseEvent(SkinEventType type,
SkinMouseButtonType button,
const QPoint& pos,
bool skipSync) {
SkinEvent* skin_event = createSkinEvent(type);
skin_event->u.mouse.button = button;
skin_event->u.mouse.skip_sync = skipSync;
skin_event->u.mouse.x = pos.x();
skin_event->u.mouse.y = pos.y();
skin_event->u.mouse.xrel = pos.x() - mPrevMousePosition.x();
skin_event->u.mouse.yrel = pos.y() - mPrevMousePosition.y();
mPrevMousePosition = pos;
queueSkinEvent(skin_event);
}
void EmulatorQtWindow::forwardKeyEventToEmulator(SkinEventType type,
QKeyEvent* event) {
SkinEvent* skin_event = createSkinEvent(type);
SkinEventKeyData& keyData = skin_event->u.key;
keyData.keycode = convertKeyCode(event->key());
Qt::KeyboardModifiers modifiers = event->modifiers();
if (modifiers & Qt::ShiftModifier)
keyData.mod |= kKeyModLShift;
if (modifiers & Qt::ControlModifier)
keyData.mod |= kKeyModLCtrl;
if (modifiers & Qt::AltModifier)
keyData.mod |= kKeyModLAlt;
queueSkinEvent(skin_event);
}
void EmulatorQtWindow::handleKeyEvent(SkinEventType type, QKeyEvent* event) {
if (!mForwardShortcutsToDevice && mInZoomMode) {
if (event->key() == Qt::Key_Control) {
if (type == kEventKeyUp) {
raise();
mOverlay.showForZoomUserHidden();
}
}
}
if (!mForwardShortcutsToDevice && !mInZoomMode &&
event->key() == Qt::Key_Control &&
event->modifiers() == Qt::ControlModifier) {
if (type == kEventKeyDown) {
raise();
mOverlay.showForMultitouch();
}
}
if (mForwardShortcutsToDevice || !mToolWindow->handleQtKeyEvent(event)) {
forwardKeyEventToEmulator(type, event);
if (type == kEventKeyDown && event->text().length() > 0) {
Qt::KeyboardModifiers mods = event->modifiers();
mods &= ~(Qt::ShiftModifier | Qt::KeypadModifier);
if (mods == 0) {
// The key event generated text without Ctrl, Alt, etc.
// Send an additional TextInput event to the emulator.
SkinEvent* skin_event = createSkinEvent(kEventTextInput);
skin_event->u.text.down = false;
strncpy((char*)skin_event->u.text.text,
(const char*)event->text().toUtf8().constData(),
sizeof(skin_event->u.text.text) - 1);
// Ensure the event's text is 0-terminated
skin_event->u.text.text[sizeof(skin_event->u.text.text) - 1] =
0;
queueSkinEvent(skin_event);
}
}
}
}
void EmulatorQtWindow::simulateKeyPress(int keyCode, int modifiers) {
SkinEvent* event = createSkinEvent(kEventKeyDown);
event->u.key.keycode = keyCode;
event->u.key.mod = modifiers;
queueSkinEvent(event);
event = createSkinEvent(kEventKeyUp);
event->u.key.keycode = keyCode;
event->u.key.mod = modifiers;
queueSkinEvent(event);
}
void EmulatorQtWindow::simulateScrollBarChanged(int x, int y) {
SkinEvent* event = createSkinEvent(kEventScrollBarChanged);
event->u.scroll.x = x;
event->u.scroll.xmax = mContainer.horizontalScrollBar()->maximum();
event->u.scroll.y = y;
event->u.scroll.ymax = mContainer.verticalScrollBar()->maximum();
queueSkinEvent(event);
}
void EmulatorQtWindow::simulateSetScale(double scale) {
// Avoid zoom and scale events clobbering each other if the user rapidly
// changes zoom levels
if (mInZoomMode && mNextIsZoom) {
return;
}
// Reset our local copy of zoom factor
mZoomFactor = 1.0;
SkinEvent* event = createSkinEvent(kEventSetScale);
event->u.window.scale = scale;
queueSkinEvent(event);
}
void EmulatorQtWindow::simulateSetZoom(double zoom) {
// Avoid zoom and scale events clobbering each other if the user rapidly
// changes zoom levels
if (mNextIsZoom || mZoomFactor == zoom) {
return;
}
// Qt Widgets do not get properly sized unless they appear at least once.
// The
// scroll bars
// *must* be properly sized in order for zoom to create the correct GLES
// subwindow, so this
// ensures they will be. This is reset as soon as the window is shown.
mContainer.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
mContainer.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
mNextIsZoom = true;
mZoomFactor = zoom;
QSize viewport = mContainer.viewportSize();
SkinEvent* event = createSkinEvent(kEventSetZoom);
event->u.window.x = viewport.width();
event->u.window.y = viewport.height();
QScrollBar* horizontal = mContainer.horizontalScrollBar();
event->u.window.scroll_h =
horizontal->isVisible() ? horizontal->height() : 0;
event->u.window.scale = zoom;
queueSkinEvent(event);
}
void EmulatorQtWindow::simulateWindowMoved(const QPoint& pos) {
SkinEvent* event = createSkinEvent(kEventWindowMoved);
event->u.window.x = pos.x();
event->u.window.y = pos.y();
queueSkinEvent(event);
mOverlay.move(mContainer.mapToGlobal(QPoint()));
}
void EmulatorQtWindow::simulateZoomedWindowResized(const QSize& size) {
SkinEvent* event = createSkinEvent(kEventZoomedWindowResized);
QScrollBar* horizontal = mContainer.horizontalScrollBar();
event->u.scroll.x = horizontal->value();
event->u.scroll.y = mContainer.verticalScrollBar()->value();
event->u.scroll.xmax = size.width();
event->u.scroll.ymax = size.height();
event->u.scroll.scroll_h =
horizontal->isVisible() ? horizontal->height() : 0;
queueSkinEvent(event);
mOverlay.resize(size);
}
void EmulatorQtWindow::setForwardShortcutsToDevice(int index) {
mForwardShortcutsToDevice = (index != 0);
}
void EmulatorQtWindow::slot_runOnUiThread(SkinGenericFunction* f,
void* data,
QSemaphore* semaphore) {
(*f)(data);
if (semaphore)
semaphore->release();
}
bool EmulatorQtWindow::isInZoomMode() const {
return mInZoomMode;
}
ToolWindow* EmulatorQtWindow::toolWindow() const {
return mToolWindow;
}
void EmulatorQtWindow::showZoomIfNotUserHidden() {
if (!mOverlay.wasZoomUserHidden()) {
mOverlay.showForZoom();
}
}
QSize EmulatorQtWindow::containerSize() const {
return mContainer.size();
}
QRect EmulatorQtWindow::deviceGeometry() const {
return mDeviceGeometry;
}
void EmulatorQtWindow::toggleZoomMode() {
mInZoomMode = !mInZoomMode;
// Exiting zoom mode snaps back to aspect ratio
if (!mInZoomMode) {
// Scroll bars should be turned off immediately.
mContainer.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
mContainer.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
doResize(mContainer.size());
mOverlay.hide();
} else {
// Once in zoom mode, the scroll bars should automatically show up
// when necessary.
mContainer.setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
mContainer.setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
mOverlay.showForZoom();
}
}
void EmulatorQtWindow::recenterFocusPoint() {
mContainer.horizontalScrollBar()->setValue(mFocus.x() * width() -
mViewportFocus.x());
mContainer.verticalScrollBar()->setValue(mFocus.y() * height() -
mViewportFocus.y());
mFocus = QPointF();
mViewportFocus = QPoint();
}
void EmulatorQtWindow::saveZoomPoints(const QPoint& focus,
const QPoint& viewportFocus) {
// The underlying frame will change sizes, so get what "percentage" of the
// frame was
// clicked, where (0,0) is the top-left corner and (1,1) is the bottom right
// corner.
mFocus = QPointF((float)focus.x() / this->width(),
(float)focus.y() / this->height());
// Save to re-align the container with the underlying frame.
mViewportFocus = viewportFocus;
}
void EmulatorQtWindow::scaleDown() {
doResize(mContainer.size() / 1.1, true);
}
void EmulatorQtWindow::scaleUp() {
doResize(mContainer.size() * 1.1, true);
}
void EmulatorQtWindow::zoomIn() {
zoomIn(QPoint(width() / 2, height() / 2),
QPoint(mContainer.width() / 2, mContainer.height() / 2));
}
void EmulatorQtWindow::zoomIn(const QPoint& focus,
const QPoint& viewportFocus) {
if (!mBackingSurface) return;
saveZoomPoints(focus, viewportFocus);
// The below scale = x creates a skin equivalent to calling "window
// scale x" through the emulator console. At scale = 1, the device
// should be at a 1:1 pixel mapping with the monitor. We allow going
// to twice this size.
double scale =
((double)size().width() / (double)mBackingSurface->original_w);
double maxZoom = mZoomFactor * 2.0 / scale;
if (scale < 2) {
simulateSetZoom(std::min(mZoomFactor + .25, maxZoom));
}
}
void EmulatorQtWindow::zoomOut() {
zoomOut(QPoint(width() / 2, height() / 2),
QPoint(mContainer.width() / 2, mContainer.height() / 2));
}
void EmulatorQtWindow::zoomOut(const QPoint& focus,
const QPoint& viewportFocus) {
saveZoomPoints(focus, viewportFocus);
if (mZoomFactor > 1) {
simulateSetZoom(std::max(mZoomFactor - .25, 1.0));
}
}
void EmulatorQtWindow::zoomReset() {
simulateSetZoom(1);
}
void EmulatorQtWindow::zoomTo(const QPoint& focus, const QSize& rectSize) {
if (!mBackingSurface) return;
saveZoomPoints(focus,
QPoint(mContainer.width() / 2, mContainer.height() / 2));
// The below scale = x creates a skin equivalent to calling "window
// scale x" through the emulator console. At scale = 1, the device
// should be at a 1:1 pixel mapping with the monitor. We allow going to
// twice this size.
double scale =
((double)size().width() / (double)mBackingSurface->original_w);
// Calculate the "ideal" zoom factor, which would perfectly frame this
// rectangle, and the "maximum" zoom factor, which makes scale = 1, and
// pick the smaller one. Adding 20 accounts for the scroll bars
// potentially cutting off parts of the selection
double maxZoom = mZoomFactor * 2.0 / scale;
double idealWidthZoom = mZoomFactor * (double)mContainer.width() /
(double)(rectSize.width() + 20);
double idealHeightZoom = mZoomFactor * (double)mContainer.height() /
(double)(rectSize.height() + 20);
simulateSetZoom(std::min({idealWidthZoom, idealHeightZoom, maxZoom}));
}
void EmulatorQtWindow::panHorizontal(bool left) {
QScrollBar* bar = mContainer.horizontalScrollBar();
if (left) {
bar->setValue(bar->value() - bar->singleStep());
} else {
bar->setValue(bar->value() + bar->singleStep());
}
}
void EmulatorQtWindow::panVertical(bool up) {
QScrollBar* bar = mContainer.verticalScrollBar();
if (up) {
bar->setValue(bar->value() - bar->singleStep());
} else {
bar->setValue(bar->value() + bar->singleStep());
}
}
bool EmulatorQtWindow::mouseInside() {
QPoint widget_cursor_coords = mapFromGlobal(QCursor::pos());
return widget_cursor_coords.x() >= 0 &&
widget_cursor_coords.x() < width() &&
widget_cursor_coords.y() >= 0 && widget_cursor_coords.y() < height();
}
void EmulatorQtWindow::wheelEvent(QWheelEvent* event) {
if (!mWheelScrollTimer.isActive()) {
handleMouseEvent(kEventMouseButtonDown, kMouseButtonLeft, event->pos());
mWheelScrollPos = event->pos();
}
mWheelScrollTimer.start();
mWheelScrollPos.setY(mWheelScrollPos.y() + event->delta() / 8);
handleMouseEvent(kEventMouseMotion, kMouseButtonLeft, mWheelScrollPos);
}
void EmulatorQtWindow::wheelScrollTimeout() {
handleMouseEvent(kEventMouseButtonUp, kMouseButtonLeft, mWheelScrollPos);
}
void EmulatorQtWindow::checkAdbVersionAndWarn() {
QSettings settings;
if (!mAdbInterface->isAdbVersionCurrent() &&
settings.value(Ui::Settings::AUTO_FIND_ADB, true).toBool()) {
mAdbWarningBox.setText(tr(
"The ADB binary found at %1 is obsolete and has serious"
"performance problems with the Android Emulator. Please update to "
"a newer version to get significantly faster app/file transfer.")
.arg(mAdbInterface->detectedAdbPath().c_str()));
QSettings settings;
if (settings.value(Ui::Settings::SHOW_ADB_WARNING, true).toBool()) {
QObject::connect(&mAdbWarningBox,
SIGNAL(buttonClicked(QAbstractButton*)), this,
SLOT(slot_adbWarningMessageAccepted()));
QCheckBox* checkbox = new QCheckBox(tr("Never show this again."));
checkbox->setCheckState(Qt::Unchecked);
mAdbWarningBox.setWindowModality(Qt::NonModal);
mAdbWarningBox.setCheckBox(checkbox);
mAdbWarningBox.show();
}
}
}
void EmulatorQtWindow::slot_adbWarningMessageAccepted() {
QCheckBox* checkbox = mAdbWarningBox.checkBox();
if (checkbox->checkState() == Qt::Checked) {
QSettings settings;
settings.setValue(Ui::Settings::SHOW_ADB_WARNING, false);
}
}
void EmulatorQtWindow::runAdbShellStopAndQuit() {
// we need to run it only once, so don't ever reset this
if (mStartedAdbStopProcess) {
return;
}
mStartedAdbStopProcess = true;
mAdbInterface->runAdbCommand(
{"shell", "stop"},
[this](const android::emulation::OptionalAdbCommandResult&) {
queueQuitEvent();
},
android::base::System::kInfinite);
}