diff --git a/qrenderdoc/Windows/Dialogs/CaptureDialog.cpp b/qrenderdoc/Windows/Dialogs/CaptureDialog.cpp index d53e4d3ef..8a66b0905 100644 --- a/qrenderdoc/Windows/Dialogs/CaptureDialog.cpp +++ b/qrenderdoc/Windows/Dialogs/CaptureDialog.cpp @@ -60,6 +60,32 @@ static QString GetDescription(const EnvironmentModification &env) Q_DECLARE_METATYPE(CaptureSettings); +void CaptureDialog::initWarning(RDLabel *warning) +{ + if(!warning) + return; + + QPalette pal = warning->palette(); + + QColor base = pal.color(QPalette::ToolTipBase); + + pal.setColor(QPalette::Foreground, pal.color(QPalette::ToolTipText)); + pal.setColor(QPalette::Window, base); + pal.setColor(QPalette::Base, base.darker(120)); + + warning->setBackgroundRole(QPalette::Window); + + QObject::connect(warning, &RDLabel::mouseMoved, + [this, warning](QMouseEvent *) { warning->setBackgroundRole(QPalette::Base); }); + QObject::connect(warning, &RDLabel::leave, + [this, warning]() { warning->setBackgroundRole(QPalette::Window); }); + + warning->setPalette(pal); + warning->setAutoFillBackground(true); + warning->setMouseTracking(true); + warning->setVisible(false); +} + CaptureDialog::CaptureDialog(ICaptureContext &ctx, OnCaptureMethod captureCallback, OnInjectMethod injectCallback, QWidget *parent) : QFrame(parent), ui(new Ui::CaptureDialog), m_Ctx(ctx) @@ -112,32 +138,20 @@ CaptureDialog::CaptureDialog(ICaptureContext &ctx, OnCaptureMethod captureCallba // sort by PID by default ui->processList->sortByColumn(1, Qt::AscendingOrder); + // Set up warning for host layer config + initWarning(ui->vulkanLayerWarn); ui->vulkanLayerWarn->setVisible(RENDERDOC_NeedVulkanLayerRegistration(NULL, NULL, NULL)); - QObject::connect(ui->vulkanLayerWarn, &RDLabel::clicked, this, &CaptureDialog::vulkanLayerWarn_mouseClick); - QPalette pal = ui->vulkanLayerWarn->palette(); + // Set up scanning for Android apps + initWarning(ui->androidScan); - QColor base = pal.color(QPalette::ToolTipBase); + // Set up warning for Android apps + initWarning(ui->androidWarn); + QObject::connect(ui->androidWarn, &RDLabel::clicked, this, &CaptureDialog::androidWarn_mouseClick); - pal.setColor(QPalette::Foreground, pal.color(QPalette::ToolTipText)); - - pal.setColor(QPalette::Window, base); - pal.setColor(QPalette::Base, base.darker(120)); - - ui->vulkanLayerWarn->setBackgroundRole(QPalette::Window); - - QObject::connect(ui->vulkanLayerWarn, &RDLabel::mouseMoved, [this](QMouseEvent *) { - ui->vulkanLayerWarn->setBackgroundRole(QPalette::Base); - }); - QObject::connect(ui->vulkanLayerWarn, &RDLabel::leave, - [this]() { ui->vulkanLayerWarn->setBackgroundRole(QPalette::Window); }); - - ui->vulkanLayerWarn->setPalette(pal); - ui->vulkanLayerWarn->setAutoFillBackground(true); - - ui->vulkanLayerWarn->setMouseTracking(true); + m_AndroidFlags = AndroidFlags::NoFlags; m_CaptureCallback = captureCallback; m_InjectCallback = injectCallback; @@ -397,6 +411,137 @@ void CaptureDialog::vulkanLayerWarn_mouseClick() } } +void CaptureDialog::CheckAndroidSetup(QString &filename) +{ + ui->androidWarn->setVisible(false); + ui->androidScan->setVisible(true); + + LambdaThread *scan = new LambdaThread([this, filename]() { + + QByteArray hostnameBytes = m_Ctx.Replay().CurrentRemote()->Hostname.toUtf8(); + RENDERDOC_CheckAndroidPackage(hostnameBytes.data(), filename.toUtf8().data(), &m_AndroidFlags); + + const bool missingLibrary = bool(m_AndroidFlags & AndroidFlags::MissingLibrary); + const bool missingPermissions = bool(m_AndroidFlags & AndroidFlags::MissingPermissions); + + if(missingLibrary || missingPermissions) + { + // Check failed - set the warning visible + GUIInvoke::call([this]() { + ui->androidScan->setVisible(false); + ui->androidWarn->setVisible(true); + }); + } + else + { + // Check passed - no warnings needed + GUIInvoke::call([this]() { + ui->androidScan->setVisible(false); + ui->androidWarn->setVisible(false); + }); + } + }); + + scan->start(); + scan->deleteLater(); +} + +void CaptureDialog::androidWarn_mouseClick() +{ + QString exe = ui->exePath->text(); + + QString caption = tr("Missing RenderDoc requirements"); + + QString msg = tr("In order to debug on Android, the following problems must be fixed:

"); + + bool missingPermissions = bool(m_AndroidFlags & AndroidFlags::MissingPermissions); + bool missingLibrary = bool(m_AndroidFlags & AndroidFlags::MissingLibrary); + + if(missingPermissions) + { + msg += + tr("Missing permissions
" + "The target APK must have the following permissions:
" + "android.permission.WRITE_EXTERNAL_STORAGE
" + "android.permission.INTERNET

"); + } + + if(missingLibrary) + { + msg += + tr("Missing library
" + "The RenderDoc library must be present in the " + "installed application.

" + "To fix this, you should repackage the APK following guidelines on the " + "" + "RenderDoc Wiki

"); + } + + if(missingPermissions) + { + // Don't prompt for patching if anything other than library is missing + RDDialog::critical(this, caption, msg); + } + else + { + msg += + tr("If you are only targeting Vulkan, RenderDoc can try to add the layer for you, " + "which requires pulling the APK, patching it, uninstalling the original, and " + "installing the modified version with a debug key. " + "This works for many debuggable applications, but not all, especially those that " + "check their integrity before launching.

" + "Your system will need several tools installed and available to RenderDoc. " + "Any missing tools will be noted in the log. Follow the steps " + "here" + " to get them.

" + "Would you like RenderDoc to try patching your APK?"); + + QMessageBox::StandardButton prompt = RDDialog::question(this, caption, msg); + + if(prompt == QMessageBox::Yes) + { + float progress = 0.0f; + bool patchSucceeded = false; + + // call into APK pull, patch, install routine, then continue + LambdaThread *patch = new LambdaThread([this, exe, &patchSucceeded, &progress]() { + QByteArray hostnameBytes = m_Ctx.Replay().CurrentRemote()->Hostname.toUtf8(); + if(RENDERDOC_AddLayerToAndroidPackage(hostnameBytes.data(), exe.toUtf8().data(), &progress)) + { + // Sucess! + patchSucceeded = true; + + RDDialog::information( + this, tr("Patch succeeded!"), + tr("The patch process succeeded and %1 now contains the RenderDoc layer").arg(exe)); + } + else + { + RDDialog::critical(this, tr("Failed to patch APK"), + tr("Something has gone wrong and APK patching failed " + "for:
%1
Check diagnostic log in Help " + "menu for more details.") + .arg(exe)); + } + }); + + patch->start(); + // wait a few ms before popping up a progress bar + patch->wait(500); + if(patch->isRunning()) + { + ShowProgressDialog(this, tr("Patching %1, please wait...").arg(exe), + [patch]() { return !patch->isRunning(); }, + [&progress]() { return progress; }); + } + patch->deleteLater(); + + if(patchSucceeded) + ui->androidWarn->setVisible(false); + } + } +} + void CaptureDialog::on_processRefesh_clicked() { fillProcessList(); @@ -446,7 +591,14 @@ void CaptureDialog::on_exePathBrowse_clicked() } if(!filename.isEmpty()) + { SetExecutableFilename(filename); + + if(m_Ctx.Replay().CurrentRemote() && m_Ctx.Replay().CurrentRemote()->IsHostADB()) + { + CheckAndroidSetup(filename); + } + } } void CaptureDialog::on_workDirBrowse_clicked() diff --git a/qrenderdoc/Windows/Dialogs/CaptureDialog.h b/qrenderdoc/Windows/Dialogs/CaptureDialog.h index 3c3dfb218..2ed50cd4b 100644 --- a/qrenderdoc/Windows/Dialogs/CaptureDialog.h +++ b/qrenderdoc/Windows/Dialogs/CaptureDialog.h @@ -35,6 +35,7 @@ class CaptureDialog; class QStandardItemModel; class LiveCapture; +class RDLabel; class CaptureDialog : public QFrame, public ICaptureDialog { @@ -97,6 +98,7 @@ private slots: // manual slots void vulkanLayerWarn_mouseClick(); + void androidWarn_mouseClick(); private: Ui::CaptureDialog *ui; @@ -110,4 +112,8 @@ private: QList m_EnvModifications; bool m_Inject; void fillProcessList(); + void initWarning(RDLabel *label); + + void CheckAndroidSetup(QString &filename); + AndroidFlags m_AndroidFlags; }; diff --git a/qrenderdoc/Windows/Dialogs/CaptureDialog.ui b/qrenderdoc/Windows/Dialogs/CaptureDialog.ui index a0141ba4e..97300a8dd 100644 --- a/qrenderdoc/Windows/Dialogs/CaptureDialog.ui +++ b/qrenderdoc/Windows/Dialogs/CaptureDialog.ui @@ -312,6 +312,50 @@ + + + + PointingHandCursor + + + QFrame::Box + + + <html><head/><body><table><tr><td valign="middle"><img src=":/information.png"/></td><td valign="middle"><p>Scanning Android application for RenderDoc support...<br/></p></td></tr></table></body></html> + + + Qt::RichText + + + :/information.png + + + 3 + + + + + + + PointingHandCursor + + + QFrame::Box + + + <html><head/><body><table><tr><td valign="middle"><img src=":/information.png"/></td><td><p>Warning: Android application not set up for RenderDoc capture.<br/>Click here for ways to fix this.</p></td></tr></table></body></html> + + + Qt::RichText + + + :/information.png + + + 3 + + + diff --git a/renderdoc/api/replay/renderdoc_replay.h b/renderdoc/api/replay/renderdoc_replay.h index fc5d5b18e..b3d5e1abd 100644 --- a/renderdoc/api/replay/renderdoc_replay.h +++ b/renderdoc/api/replay/renderdoc_replay.h @@ -1557,3 +1557,13 @@ extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_EnumerateAndroidDevices(rdc DOCUMENT("Internal function for starting an android remote server."); extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_StartAndroidRemoteServer(const char *device); + +DOCUMENT("Internal function for checking remote Android package for requirements"); +extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_CheckAndroidPackage(const char *host, + const char *exe, + AndroidFlags *flags); + +DOCUMENT("Internal function that attempts to modify APK contents, adding Vulkan layer."); +extern "C" RENDERDOC_API bool RENDERDOC_CC RENDERDOC_AddLayerToAndroidPackage(const char *host, + const char *exe, + float *progress); diff --git a/renderdoc/api/replay/replay_enums.h b/renderdoc/api/replay/replay_enums.h index 480132044..c5c470014 100644 --- a/renderdoc/api/replay/replay_enums.h +++ b/renderdoc/api/replay/replay_enums.h @@ -3301,4 +3301,41 @@ enum class VulkanLayerFlags : uint32_t Unfixable = 0x40, }; -BITMASK_OPERATORS(VulkanLayerFlags); \ No newline at end of file +BITMASK_OPERATORS(VulkanLayerFlags); + +DOCUMENT(R"(A set of flags giving details of the current status of Android tracability. + +.. data:: NoFlags + + There are no problems with the Android application setup. + +.. data:: MissingLibrary + + The RenderDoc library (whether Vulkan layer or OpenGLES library) could not be found in the + application or system locations. + +.. data:: MissingPermissions + + The application being checked does not have the requesite permission: + + android.permission.WRITE_EXTERNAL_STORAGE + android.permission.INTERNET + +.. data:: NotDebuggable + + The application is not debuggable. + +.. data:: Unfixable + + The current situation is not fixable automatically and requires user intervention/disambiguation. +)"); +enum class AndroidFlags : uint32_t +{ + NoFlags = 0x0, + MissingLibrary = 0x1, + MissingPermissions = 0x2, + NotDebuggable = 0x4, + Unfixable = 0x8, +}; + +BITMASK_OPERATORS(AndroidFlags); \ No newline at end of file diff --git a/renderdoc/replay/entry_points.cpp b/renderdoc/replay/entry_points.cpp index 69d03387e..0c90fe247 100644 --- a/renderdoc/replay/entry_points.cpp +++ b/renderdoc/replay/entry_points.cpp @@ -687,8 +687,324 @@ uint32_t StartAndroidPackageForCapture(const char *host, const char *package) return ret; } + +bool SearchForAndroidLayer(const string &deviceID, const string &location, const string &layerName) +{ + RDCLOG("Checking for layers in: %s", location.c_str()); + string findLayer = + adbExecCommand(deviceID, "shell find " + location + " -name " + layerName).strStdout; + if(!findLayer.empty()) + { + RDCLOG("Found RenderDoc layer in %s", location.c_str()); + return true; + } + return false; } +bool RemoveAPKSignature(const string &apk) +{ + RDCLOG("Checking for existing signature"); + + // Get the list of files in META-INF + string fileList = execCommand("aapt list " + apk).strStdout; + if(fileList.empty()) + return false; + + // Walk through the output. If it starts with META-INF, remove it. + uint32_t fileCount = 0; + uint32_t matchCount = 0; + std::istringstream contents(fileList); + string line; + string prefix("META-INF"); + while(std::getline(contents, line)) + { + line = trim(line); + fileCount++; + if(line.compare(0, prefix.size(), prefix) == 0) + { + RDCDEBUG("Match found, removing %s", line.c_str()); + execCommand("aapt remove " + apk + " " + line); + matchCount++; + } + } + RDCLOG("%d files searched, %d removed", fileCount, matchCount); + + // Ensure no hits on second pass through + RDCDEBUG("Walk through file list again, ensure signature removed"); + fileList = execCommand("aapt list " + apk).strStdout; + std::istringstream recheck(fileList); + while(std::getline(recheck, line)) + { + if(line.compare(0, prefix.size(), prefix) == 0) + { + RDCERR("Match found, that means removal failed! %s", line.c_str()); + return false; + } + } + return true; +} + +bool AddLayerToAPK(const string &apk, const string &layerPath, const string &layerName, + const string &abi, const string &tmpDir) +{ + RDCLOG("Adding RenderDoc layer"); + + // Run aapt from the directory containing "lib" so the relative paths are good + string relativeLayer("lib/" + abi + "/" + layerName); + string workDir = removeFromEnd(layerPath, relativeLayer); + Process::ProcessResult result = execCommand("aapt add " + apk + " " + relativeLayer, workDir); + + if(result.strStdout.empty()) + { + RDCERR("Failed to add layer to APK. STDERR: %s", result.strStderror.c_str()); + return false; + } + + return true; +} + +bool RealignAPK(const string &apk, string &alignedAPK, const string &tmpDir) +{ + // Re-align the APK for performance + RDCLOG("Realigning APK"); + string errOut = execCommand("zipalign -f 4 " + apk + " " + alignedAPK, tmpDir).strStderror; + + if(!errOut.empty()) + return false; + + // Wait until the aligned version exists to proceed + uint32_t elapsed = 0; + uint32_t timeout = 10000; // 10 seconds + while(elapsed < timeout) + { + if(FileIO::exists(alignedAPK.c_str())) + { + RDCLOG("Aligned APK ready to go, continuing..."); + return true; + } + + Threading::Sleep(1000); + elapsed += 1000; + } + + RDCERR("Timeout reached aligning APK"); + return false; +} + +string GetAndroidDebugKey() +{ + string key = FileIO::GetTempFolderFilename() + "debug.keystore"; + + if(FileIO::exists(key.c_str())) + return key; + + string create = "keytool"; + create += " -genkey"; + create += " -keystore " + key; + create += " -storepass android"; + create += " -alias androiddebugkey"; + create += " -keypass android"; + create += " -keyalg RSA"; + create += " -keysize 2048"; + create += " -validity 10000"; + create += " -dname \"CN=, OU=, O=, L=, S=, C=\""; + + Process::ProcessResult result = execCommand(create); + + if(!result.strStderror.empty()) + RDCERR("Failed to create debug key"); + + return key; +} +bool DebugSignAPK(const string &apk, const string &workDir) +{ + RDCLOG("Signing with debug key"); + + string debugKey = GetAndroidDebugKey(); + + string args; + args += " sign "; + args += " --ks " + debugKey + " "; + args += " --ks-pass pass:android "; + args += " --key-pass pass:android "; + args += " --ks-key-alias androiddebugkey "; + args += apk; + execScript("apksigner", args.c_str(), workDir.c_str()); + + // Check for signature + string list = execCommand("aapt list " + apk).strStdout; + + // Walk through the output. If it starts with META-INF, we're good + std::istringstream contents(list); + string line; + string prefix("META-INF"); + while(std::getline(contents, line)) + { + if(line.compare(0, prefix.size(), prefix) == 0) + { + RDCLOG("Signature found, continuing..."); + return true; + } + } + + RDCERR("re-sign of APK failed!"); + return false; +} + +bool UninstallOriginalAPK(const string &deviceID, const string &packageName, const string &workDir) +{ + RDCLOG("Uninstalling previous version of application"); + + execCommand("adb uninstall " + packageName, workDir); + + // Wait until uninstall completes + string uninstallResult; + uint32_t elapsed = 0; + uint32_t timeout = 10000; // 10 seconds + while(elapsed < timeout) + { + uninstallResult = adbExecCommand(deviceID, "shell pm path " + packageName).strStdout; + if(uninstallResult.empty()) + { + RDCLOG("Package removed"); + return true; + } + + Threading::Sleep(1000); + elapsed += 1000; + } + + RDCERR("Uninstallation of APK failed!"); + return false; +} + +bool ReinstallPatchedAPK(const string &deviceID, const string &apk, const string &abi, + const string &packageName, const string &workDir) +{ + RDCLOG("Reinstalling APK"); + + execCommand("adb install --abi " + abi + " " + apk, workDir); + + // Wait until re-install completes + string reinstallResult; + uint32_t elapsed = 0; + uint32_t timeout = 10000; // 10 seconds + while(elapsed < timeout) + { + reinstallResult = adbExecCommand(deviceID, "shell pm path " + packageName).strStdout; + if(!reinstallResult.empty()) + { + RDCLOG("Patched APK reinstalled, continuing..."); + return true; + } + + Threading::Sleep(1000); + elapsed += 1000; + } + + RDCERR("Reinstallation of APK failed!"); + return false; +} + +bool CheckPatchingRequirements() +{ + // check for aapt, zipalign, apksigner, debug key + vector requirements; + vector missingTools; + requirements.push_back("aapt"); + requirements.push_back("zipalign"); + requirements.push_back("keytool"); + requirements.push_back("apksigner"); + requirements.push_back("java"); + + for(uint32_t i = 0; i < requirements.size(); i++) + { + if(FileIO::FindFileInPath(requirements[i]).empty()) + missingTools.push_back(requirements[i]); + } + + if(missingTools.size() > 0) + { + for(uint32_t i = 0; i < missingTools.size(); i++) + RDCERR("Missing %s", missingTools[i].c_str()); + return false; + } + + return true; +} + +bool PullAPK(const string &deviceID, const string &pkgPath, const string &apk) +{ + RDCLOG("Pulling APK to patch"); + + adbExecCommand(deviceID, "pull " + pkgPath + " " + apk); + + // Wait until the apk lands + uint32_t elapsed = 0; + uint32_t timeout = 10000; // 10 seconds + while(elapsed < timeout) + { + if(FileIO::exists(apk.c_str())) + { + RDCLOG("Original APK ready to go, continuing..."); + return true; + } + + Threading::Sleep(1000); + elapsed += 1000; + } + + RDCERR("Failed to pull APK"); + return false; +} + +bool CheckPermissions(const string &dump) +{ + if(dump.find("android.permission.WRITE_EXTERNAL_STORAGE") == string::npos) + { + RDCWARN("APK missing WRITE_EXTERNAL_STORAGE permission"); + return false; + } + + if(dump.find("android.permission.INTERNET") == string::npos) + { + RDCWARN("APK missing INTERNET permission"); + return false; + } + + return true; +} + +bool CheckAPKPermissions(const string &apk) +{ + RDCLOG("Checking that APK can be can write to sdcard"); + + string badging = execCommand("aapt dump badging " + apk).strStdout; + + if(badging.empty()) + { + RDCERR("Unable to aapt dump %s", apk.c_str()); + return false; + } + + return CheckPermissions(badging); +} +bool CheckDebuggable(const string &apk) +{ + RDCLOG("Checking that APK s debuggable"); + + string badging = execCommand("aapt dump badging " + apk).strStdout; + + if(badging.find("application-debuggable")) + { + RDCERR("APK is not debuggable"); + return false; + } + + return true; +} +} // namespace Android + using namespace Android; extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_GetAndroidFriendlyName(const rdctype::str &device, rdctype::str &friendly) @@ -863,7 +1179,8 @@ bool installRenderDocServer(const string &deviceID) } // Ensure installation succeeded - string adbCheck = adbExecCommand(deviceID, "shell pm list packages org.renderdoc.renderdoccmd").strStdout; + string adbCheck = + adbExecCommand(deviceID, "shell pm list packages org.renderdoc.renderdoccmd").strStdout; if(adbCheck.empty()) { RDCERR("Installation of RenderDocCmd.apk failed!"); @@ -884,7 +1201,8 @@ extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_StartAndroidRemoteServer(co // We should hook up versioning of the server, then re-install if the version is old or mismatched // But for now, just install it, if not already present - string adbPackage = adbExecCommand(deviceID, "shell pm list packages org.renderdoc.renderdoccmd").strStdout; + string adbPackage = + adbExecCommand(deviceID, "shell pm list packages org.renderdoc.renderdoccmd").strStdout; if(adbPackage.empty()) { if(!installRenderDocServer(deviceID)) @@ -899,6 +1217,242 @@ extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_StartAndroidRemoteServer(co "shell am start -n org.renderdoc.renderdoccmd/.Loader -e renderdoccmd remoteserver"); } +bool CheckInstalledPermissions(const string &deviceID, const string &packageName) +{ + RDCLOG("Checking installed permissions for %s", packageName.c_str()); + + string dump = adbExecCommand(deviceID, "shell pm dump " + packageName).strStdout; + if(dump.empty()) + RDCERR("Unable to pm dump %s", packageName.c_str()); + + return CheckPermissions(dump); +} + +extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_CheckAndroidPackage(const char *host, + const char *exe, + AndroidFlags *flags) +{ + string packageName(basename(string(exe))); + + int index = 0; + std::string deviceID; + Android::extractDeviceIDAndIndex(host, index, deviceID); + + // Find the path to package + string pkgPath = trim(adbExecCommand(deviceID, "shell pm path " + packageName).strStdout); + pkgPath.erase(pkgPath.begin(), pkgPath.begin() + strlen("package:")); + pkgPath.erase(pkgPath.end() - strlen("base.apk"), pkgPath.end()); + pkgPath += "lib"; + + string layerName = "libVkLayer_GLES_RenderDoc.so"; + + // Reset the flags each time we check + *flags = AndroidFlags::NoFlags; + + bool found = false; + + // First, see if the application contains the layer + if(SearchForAndroidLayer(deviceID, pkgPath, layerName)) + found = true; + + // Next, check a debug location only usable by rooted devices + if(!found && SearchForAndroidLayer(deviceID, "/data/local/debug/vulkan", layerName)) + found = true; + + // TODO: Add any future layer locations + + if(!found) + { + RDCWARN("No RenderDoc layer for Vulkan or GLES was found"); + *flags |= AndroidFlags::MissingLibrary; + } + + // Next check permissions of the installed application (without pulling the APK) + if(!CheckInstalledPermissions(deviceID, packageName)) + { + RDCWARN("Android application does not have required permissions"); + *flags |= AndroidFlags::MissingPermissions; + } + + return; +} + +string DetermineInstalledABI(const string &deviceID, const string &packageName) +{ + RDCLOG("Checking installed ABI for %s", packageName.c_str()); + string abi; + + string dump = adbExecCommand(deviceID, "shell pm dump " + packageName).strStdout; + if(dump.empty()) + RDCERR("Unable to pm dump %s", packageName.c_str()); + + // Walk through the output and look for primaryCpuAbi + std::istringstream contents(dump); + string line; + string prefix("primaryCpuAbi="); + while(std::getline(contents, line)) + { + line = trim(line); + if(line.compare(0, prefix.size(), prefix) == 0) + { + // Extract the abi + abi = line.substr(line.find_last_of("=") + 1); + RDCLOG("primaryCpuAbi found: %s", abi.c_str()); + break; + } + } + + if(abi.empty()) + RDCERR("Unable to determine installed abi for: %s", packageName.c_str()); + + return abi; +} + +string FindAndroidLayer(const string &abi, const string &layerName) +{ + string layer; + + // Check known paths for RenderDoc layer + string exePath; + FileIO::GetExecutableFilename(exePath); + string exeDir = dirname(FileIO::GetFullPathname(exePath)); + + std::vector paths; + +#if defined(RENDERDOC_LAYER_PATH) + string customPath(RENDERDOC_LAYER_PATH); + RDCLOG("Custom layer path: %s", customPath.c_str()); + + if(FileIO::IsRelativePath(customPath)) + customPath = exeDir + "/" + customPath; + + if(!endswith(customPath, "/")) + customPath += "/"; + + // Custom path must point to directory containing ABI folders + customPath += abi; + if(!FileIO::exists(customPath.c_str())) + { + RDCWARN("Custom layer path does not contain required ABI"); + } + paths.push_back(customPath + "/" + layerName); +#endif + + string windows = "/android/lib/"; + string linux = "/../share/renderdoc/android/lib/"; + string local = "/../../build-android/renderdoccmd/libs/lib/"; + string macOS = "/../../../../../build-android/renderdoccmd/libs/lib/"; + + paths.push_back(exeDir + windows + abi + "/" + layerName); + paths.push_back(exeDir + linux + abi + "/" + layerName); + paths.push_back(exeDir + local + abi + "/" + layerName); + paths.push_back(exeDir + macOS + abi + "/" + layerName); + + for(uint32_t i = 0; i < paths.size(); i++) + { + RDCLOG("Checking for layer in %s", paths[i].c_str()); + if(FileIO::exists(paths[i].c_str())) + { + layer = paths[i]; + RDCLOG("Layer found!: %s", layer.c_str()); + break; + } + } + + if(layer.empty()) + { + RDCERR( + "%s missing! RenderDoc for Android will not work without it. " + "Build your Android ABI in build-android in the root to have it " + "automatically found and installed.", + layer.c_str()); + } + + return layer; +} + +extern "C" RENDERDOC_API bool RENDERDOC_CC RENDERDOC_AddLayerToAndroidPackage(const char *host, + const char *exe, + float *progress) +{ + Process::ProcessResult result = {}; + string packageName(basename(string(exe))); + + int index = 0; + std::string deviceID; + Android::extractDeviceIDAndIndex(host, index, deviceID); + + *progress = 0.0f; + + if(!CheckPatchingRequirements()) + return false; + + *progress = 0.11f; + + // Detect which ABI was installed on the device + string abi = DetermineInstalledABI(deviceID, packageName); + + // Find the layer on host + string layerName("libVkLayer_GLES_RenderDoc.so"); + string layerPath = FindAndroidLayer(abi, layerName); + if(layerPath.empty()) + return false; + + // Find the APK on the device + string pkgPath = trim(adbExecCommand(deviceID, "shell pm path " + packageName).strStdout); + pkgPath.erase(pkgPath.begin(), pkgPath.begin() + strlen("package:")); + + string tmpDir = FileIO::GetTempFolderFilename(); + string origAPK(tmpDir + packageName + ".orig.apk"); + string alignedAPK(origAPK + ".aligned.apk"); + + *progress = 0.21f; + + // Try the following steps, bailing if anything fails + if(!PullAPK(deviceID, pkgPath, origAPK)) + return false; + + *progress = 0.31f; + + if(!CheckAPKPermissions(origAPK)) + return false; + + *progress = 0.41f; + + if(!RemoveAPKSignature(origAPK)) + return false; + + *progress = 0.51f; + + if(!AddLayerToAPK(origAPK, layerPath, layerName, abi, tmpDir)) + return false; + + *progress = 0.61f; + + if(!RealignAPK(origAPK, alignedAPK, tmpDir)) + return false; + + *progress = 0.71f; + + if(!DebugSignAPK(alignedAPK, tmpDir)) + return false; + + *progress = 0.81f; + + if(!UninstallOriginalAPK(deviceID, packageName, tmpDir)) + return false; + + *progress = 0.91f; + + if(!ReinstallPatchedAPK(deviceID, alignedAPK, abi, packageName, tmpDir)) + return false; + + *progress = 1.0f; + + // All clean! + return true; +} + extern "C" RENDERDOC_API bool RENDERDOC_CC RENDERDOC_NeedVulkanLayerRegistration( VulkanLayerFlags *flagsPtr, rdctype::array *myJSONsPtr, rdctype::array *otherJSONsPtr) diff --git a/renderdoccmd/CMakeLists.txt b/renderdoccmd/CMakeLists.txt index 388501fb6..c748dccd5 100644 --- a/renderdoccmd/CMakeLists.txt +++ b/renderdoccmd/CMakeLists.txt @@ -72,5 +72,8 @@ if(ANDROID) COMMAND ${CMAKE_COMMAND} -E copy $ libs/${ANDROID_ABI}/$ COMMAND android update project --path . --name RenderDocCmd --target ${APK_TARGET_ID} COMMAND ant debug - COMMAND ${CMAKE_COMMAND} -E copy bin/RenderDocCmd-debug.apk ${APK_FILE}) + COMMAND ${CMAKE_COMMAND} -E copy bin/RenderDocCmd-debug.apk ${APK_FILE} + COMMAND ${CMAKE_COMMAND} -E make_directory libs/lib + COMMAND ${CMAKE_COMMAND} -E copy_directory libs/${ANDROID_ABI} libs/lib/${ANDROID_ABI} + COMMAND ${CMAKE_COMMAND} -E remove_directory libs/${ANDROID_ABI}) endif()