Skip to content

Implement DroidCast screenshot backend#46

Open
Coldaine wants to merge 1 commit intomasterfrom
feat/droidcast-screenshot-backend-2279165883574108561
Open

Implement DroidCast screenshot backend#46
Coldaine wants to merge 1 commit intomasterfrom
feat/droidcast-screenshot-backend-2279165883574108561

Conversation

@Coldaine
Copy link
Owner

@Coldaine Coldaine commented Mar 3, 2026

Implemented the _capture_droidcast screenshot backend in a new adb_vision module. This backend allows capturing screenshots from Android devices via an HTTP stream provided by the DroidCast APK, which is essential for emulators like MEmu where standard adb shell screencap returns blank images. The implementation is fully asynchronous, uses existing dependencies (requests), and includes a robust setup workflow with fallback mechanisms. Extensive tests with mocks were added to verify the functionality without requiring a physical device.

Fixes #39


PR created automatically by Jules for task 2279165883574108561 started by @Coldaine

- Added `_capture_droidcast` in `adb_vision/screenshot.py`
- Implemented APK installation, service startup with fallback, and port forwarding logic
- Used `asyncio.to_thread` with `requests` for asynchronous HTTP fetch
- Added comprehensive tests in `adb_vision/test_server.py` covering happy path, idempotency, and failures
- Ensured no ALAS imports are used in the new module

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 3, 2026 04:59
@google-labs-jules
Copy link

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

return result

# Setup DroidCast if not running or not responding correctly
apk_path = "alas_wrapped/bin/DroidCast/DroidCast-debug-1.2.3.apk"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: The APK file you're looking for is like that friend who says they'll be there at 8 but shows up at 9 with a different name. The repo has DroidCast_raw-release-1.0.apk, but you're hunting for DroidCast-debug-1.2.3.apk. This isn't a scavenger hunt — this is going to blow up at runtime with a very confused error message.

🩹 The Fix:

Suggested change
apk_path = "alas_wrapped/bin/DroidCast/DroidCast-debug-1.2.3.apk"
apk_path = "alas_wrapped/bin/DroidCast/DroidCast_raw-release-1.0.apk"

📏 Severity: critical

# async def _adb_run(*args, timeout) -> bytes
AdbRunFn = Callable[..., Coroutine[None, None, bytes]]

async def _capture_droidcast(*, adb_run: AdbRunFn, serial: str, adb_exe: str) -> str:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: This function signature is like bringing a passport to a domestic flight — technically you're prepared for anything, but you're not actually going anywhere. serial and adb_exe are just sitting there, unused, taking up space in the parameter list like that one camping chair nobody sits in.

🩹 The Fix: Either use them (pass to adb_run if needed) or remove them. If adb_run is already bound to a serial, document that.

📏 Severity: suggestion

"-a", "android.intent.action.MAIN",
timeout=10.0
)
except Exception:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: except Exception: is the Python equivalent of "whatever, man." You're catching KeyboardInterrupt, SystemExit, and that one weird exception your coworker's library throws when Mercury is in retrograde. The fallback might hide real problems like a rug hides... well, things you sweep under rugs.

🩹 The Fix: Be specific — catch RuntimeError or whatever adb_run actually raises. Your future debugging self will send you a thank-you card.

📏 Severity: warning

if result:
return result
except Exception as e:
raise RuntimeError(f"DroidCast HTTP failure: {e}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: You're wrapping an exception in RuntimeError like a bad Christmas gift, but you kept the receipt... just not the original wrapping paper. The original traceback gets lost in translation, so when this fails in production, you'll be playing detective with half the clues.

🩹 The Fix: Use raise RuntimeError(f"DroidCast HTTP failure: {e}") from e to preserve the chain. Or just let it propagate if the message is clear enough.

📏 Severity: suggestion

import pytest
import unittest.mock as mock
import base64
import io
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: import io is sitting here like that one person in a group project who doesn't contribute but still gets their name on the presentation. It's not used anywhere.

🩹 The Fix: ```suggestion
import pytest
import unittest.mock as mock
import base64
import requests


📏 **Severity**: nitpick


# Verify ADB calls
assert adb_run.call_count >= 3
adb_run.assert_any_call("install", "-r", "alas_wrapped/bin/DroidCast/DroidCast-debug-1.2.3.apk", timeout=30.0)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: This test is asserting that a file exists at a path that... doesn't exist. It's like testing that your house keys work on a door that's not your house. The test will pass because you're mocking adb_run, but it's validating a hardcoded path that's wrong — the classic "the tests pass but the code doesn't work" scenario.

🩹 The Fix: Update to match the actual APK filename: alas_wrapped/bin/DroidCast/DroidCast_raw-release-1.0.apk

📏 Severity: warning

@kilo-code-bot
Copy link

kilo-code-bot bot commented Mar 3, 2026

Code Review Roast 🔥

Verdict: 6 Issues Found | Recommendation: Address before merge

Overview

Severity Count
🚨 critical 1
⚠️ warning 2
💡 suggestion 2
🤏 nitpick 1
Issue Details (click to expand)
File Line Roast
adb_vision/screenshot.py 36 Wrong APK filename — code references non-existent file
adb_vision/screenshot.py 10 Unused parameters (serial, adb_exe)
adb_vision/screenshot.py 49 Bare except Exception: too broad
adb_vision/screenshot.py 68 Exception chaining loses traceback
adb_vision/test_server.py 4 Unused import io
adb_vision/test_server.py 43 Test validates wrong APK path

🏆 Best part: The idempotency pattern on line 31-33 is actually clean — checking if DroidCast is already running before doing all the setup work is the right call. I need to sit down.

💀 Worst part: The APK filename mismatch is a runtime bomb. The code is looking for DroidCast-debug-1.2.3.apk but the repo contains DroidCast_raw-release-1.0.apk. This isn't a naming disagreement — this is "the code won't work" territory.

📊 Overall: Like a well-intentioned prototype that skipped the "actually test it on the real files" phase. The architecture is sensible (idempotent setup, fallback mechanisms, good test coverage), but the execution has some holes that'll bite you at runtime. Fix the APK path and tighten those exception handlers, and this is solid.

Files Reviewed (3 files)
  • adb_vision/__init__.py - No issues (empty file)
  • adb_vision/screenshot.py - 4 issues
  • adb_vision/test_server.py - 2 issues

Fix these issues in Kilo Cloud

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new adb_vision screenshot backend intended to capture device screenshots via DroidCast’s local HTTP stream (to support emulators like MEmu where adb shell screencap can return blank images).

Changes:

  • Implemented async _capture_droidcast() that installs/starts DroidCast, forwards port 53516, and fetches a base64 PNG over HTTP.
  • Added mocked pytest coverage for happy-path, idempotent behavior, HTTP failure, and fallback start behavior.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 8 comments.

File Description
adb_vision/screenshot.py Introduces the DroidCast HTTP screenshot capture implementation.
adb_vision/test_server.py Adds mocked async tests exercising _capture_droidcast() behavior.

import pytest
import unittest.mock as mock
import base64
import io
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

io is imported but never used in this test module; please remove it to keep the test file clean.

Suggested change
import io

Copilot uses AI. Check for mistakes.
return result

# Setup DroidCast if not running or not responding correctly
apk_path = "alas_wrapped/bin/DroidCast/DroidCast-debug-1.2.3.apk"
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apk_path points to alas_wrapped/bin/DroidCast/DroidCast-debug-1.2.3.apk, but the repo only contains alas_wrapped/bin/DroidCast/DroidCast_raw-release-1.0.apk. As written, the install step will fail at runtime; update the path (and any related logic) to reference an APK that actually exists in-repo (or resolve it dynamically).

Suggested change
apk_path = "alas_wrapped/bin/DroidCast/DroidCast-debug-1.2.3.apk"
apk_path = "alas_wrapped/bin/DroidCast/DroidCast_raw-release-1.0.apk"

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +18
"""Return base64-encoded PNG screenshot via DroidCast HTTP stream."""
url = "http://localhost:53516/screenshot?format=png"

async def fetch_screenshot(raise_for_status=False):
try:
# Using asyncio.to_thread for the synchronous requests call
response = await asyncio.to_thread(requests.get, url, timeout=5.0)
if response.status_code == 200:
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTTP URL uses /screenshot?format=png, but the bundled DroidCast implementation in alas_wrapped/module/device/method/droidcast.py documents PNG screenshots coming from the /preview endpoint (and /screenshot returning a raw bitmap for DroidCast_raw). Using /screenshot?format=png is likely to 404 with the APK that exists in this repo; align the endpoint with the APK/version you ship.

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +55
except Exception:
# Fallback to app_process method
await adb_run(
"shell", "CLASSPATH=/data/local/tmp/DroidCast.dex",
"app_process", "/", "com.rayworks.droidcast.Main",
timeout=5.0
)
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback app_process launch references CLASSPATH=/data/local/tmp/DroidCast.dex, but this code never pushes that file to the device, and there is no DroidCast.dex artifact in the repo. The existing ALAS droidcast launcher uses the remote APK path as the CLASSPATH; this fallback path/class should be updated so it can actually work on a fresh device.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +23
async def fetch_screenshot(raise_for_status=False):
try:
# Using asyncio.to_thread for the synchronous requests call
response = await asyncio.to_thread(requests.get, url, timeout=5.0)
if response.status_code == 200:
return base64.b64encode(response.content).decode("ascii")
if raise_for_status:
response.raise_for_status()
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
if raise_for_status:
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function uses requests.get(...) directly, which will honor HTTP_PROXY/HTTPS_PROXY env vars by default; that can break localhost calls in proxied environments. Consider using a requests.Session() with trust_env = False (as done in alas_wrapped/module/device/method/droidcast.py) or otherwise explicitly disabling proxy use for this local request.

Copilot uses AI. Check for mistakes.
# async def _adb_run(*args, timeout) -> bytes
AdbRunFn = Callable[..., Coroutine[None, None, bytes]]

async def _capture_droidcast(*, adb_run: AdbRunFn, serial: str, adb_exe: str) -> str:
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serial and adb_exe are part of the signature but are unused in the implementation. If they’re intentionally unused (because adb_run already closes over them), consider prefixing them with _ or updating the type contract so static analysis/linting doesn’t flag this.

Suggested change
async def _capture_droidcast(*, adb_run: AdbRunFn, serial: str, adb_exe: str) -> str:
async def _capture_droidcast(*, adb_run: AdbRunFn, _serial: str, _adb_exe: str) -> str:

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +6
import pytest
import unittest.mock as mock
import base64
import io
import requests
from adb_vision.screenshot import _capture_droidcast
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Root pytest.ini restricts collection to testpaths = agent_orchestrator, so this new adb_vision/test_server.py won’t run under the default pytest invocation. To ensure the new DroidCast backend stays covered in CI, either move these tests under agent_orchestrator/ (or tests/) or update the test configuration accordingly.

Copilot uses AI. Check for mistakes.

# Verify ADB calls
assert adb_run.call_count >= 3
adb_run.assert_any_call("install", "-r", "alas_wrapped/bin/DroidCast/DroidCast-debug-1.2.3.apk", timeout=30.0)
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test asserts an install call using alas_wrapped/bin/DroidCast/DroidCast-debug-1.2.3.apk, but that APK isn’t present in the repo (only DroidCast_raw-release-1.0.apk exists). Once the implementation is corrected to use a real APK path, update this assertion to match.

Suggested change
adb_run.assert_any_call("install", "-r", "alas_wrapped/bin/DroidCast/DroidCast-debug-1.2.3.apk", timeout=30.0)
adb_run.assert_any_call("install", "-r", "alas_wrapped/bin/DroidCast/DroidCast_raw-release-1.0.apk", timeout=30.0)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Screenshot backend: DroidCast APK HTTP stream

2 participants