Conversation
- 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>
|
👋 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 New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
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" |
There was a problem hiding this comment.
🔥 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:
| 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: |
There was a problem hiding this comment.
🔥 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: |
There was a problem hiding this comment.
🔥 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}") |
There was a problem hiding this comment.
🔥 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 |
There was a problem hiding this comment.
🔥 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) |
There was a problem hiding this comment.
🔥 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
Code Review Roast 🔥Verdict: 6 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)
🏆 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 📊 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)
|
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
io is imported but never used in this test module; please remove it to keep the test file clean.
| import io |
| return result | ||
|
|
||
| # Setup DroidCast if not running or not responding correctly | ||
| apk_path = "alas_wrapped/bin/DroidCast/DroidCast-debug-1.2.3.apk" |
There was a problem hiding this comment.
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).
| apk_path = "alas_wrapped/bin/DroidCast/DroidCast-debug-1.2.3.apk" | |
| apk_path = "alas_wrapped/bin/DroidCast/DroidCast_raw-release-1.0.apk" |
| """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: |
There was a problem hiding this comment.
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.
| 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 | ||
| ) |
There was a problem hiding this comment.
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.
| 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: |
There was a problem hiding this comment.
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.
| # 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: |
There was a problem hiding this comment.
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.
| 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: |
| import pytest | ||
| import unittest.mock as mock | ||
| import base64 | ||
| import io | ||
| import requests | ||
| from adb_vision.screenshot import _capture_droidcast |
There was a problem hiding this comment.
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.
|
|
||
| # 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) |
There was a problem hiding this comment.
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.
| 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) |
Implemented the
_capture_droidcastscreenshot backend in a newadb_visionmodule. 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 standardadb shell screencapreturns 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