diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 73ba1a7d..64200548 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -2866,6 +2866,18 @@ "authentication": "ON_INSTALL" }, "category": "Coding" + }, + { + "name": "android", + "source": { + "source": "local", + "path": "./plugins/android" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Coding" } ] } diff --git a/plugins/android/.codex-plugin/plugin.json b/plugins/android/.codex-plugin/plugin.json new file mode 100644 index 00000000..2f058642 --- /dev/null +++ b/plugins/android/.codex-plugin/plugin.json @@ -0,0 +1,41 @@ +{ + "name": "android", + "version": "0.1.0", + "description": "Android emulator QA workflows for adb-driven launch, input, UI-tree inspection, screenshots, and logcat capture.", + "author": { + "name": "OpenAI", + "email": "support@openai.com", + "url": "https://openai.com/" + }, + "homepage": "https://openai.com/", + "repository": "https://github.com/openai/plugins", + "license": "MIT", + "keywords": [ + "android", + "adb", + "emulator", + "qa", + "logcat", + "uiautomator" + ], + "skills": "./skills/", + "interface": { + "displayName": "Android", + "shortDescription": "Drive Android emulator QA workflows with adb", + "longDescription": "Use Android Emulator QA to build and install app variants, drive a booted Android emulator with adb input events, inspect UI trees, capture screenshots, and collect logcat output while reproducing issues.", + "developerName": "OpenAI", + "category": "Coding", + "capabilities": [ + "Interactive", + "Read" + ], + "websiteURL": "https://openai.com/", + "privacyPolicyURL": "https://openai.com/policies/privacy-policy/", + "termsOfServiceURL": "https://openai.com/policies/terms-of-use/", + "defaultPrompt": [ + "Use Android to reproduce an emulator issue with adb, then capture screenshots and logcat." + ], + "brandColor": "#3DDC84", + "screenshots": [] + } +} diff --git a/plugins/android/agents/openai.yaml b/plugins/android/agents/openai.yaml new file mode 100644 index 00000000..a4bb44e8 --- /dev/null +++ b/plugins/android/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Android" + short_description: "Drive Android emulator QA workflows with adb" + default_prompt: "Use Android to build an app, launch it on a booted emulator, reproduce the issue with adb-driven UI steps, and capture screenshots plus logs." diff --git a/plugins/android/plugin.lock.json b/plugins/android/plugin.lock.json new file mode 100644 index 00000000..b66d4593 --- /dev/null +++ b/plugins/android/plugin.lock.json @@ -0,0 +1,20 @@ +{ + "lockVersion": 1, + "pluginId": "com.openai.android", + "pluginVersion": "0.1.0", + "generatedBy": "codex plugin pack (draft)", + "generatedAt": "2026-03-21T16:20:57Z", + "skills": [ + { + "id": "android-emulator-qa", + "vendoredPath": "skills/android-emulator-qa", + "source": { + "type": "github", + "repo": "openai/skills-internal", + "path": ".codex/skills/android-emulator-qa", + "ref": "5a9bf9e14d3e8bdd93f046c01437a2e2f4369b86" + }, + "integrity": "sha256-de517708b4f488f820c5cc088bc6feb875b02e06597ce09372332cbdcdfcc47d" + } + ] +} diff --git a/plugins/android/skills/android-emulator-qa/LICENSE.txt b/plugins/android/skills/android-emulator-qa/LICENSE.txt new file mode 100644 index 00000000..13e25df8 --- /dev/null +++ b/plugins/android/skills/android-emulator-qa/LICENSE.txt @@ -0,0 +1,201 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf of + any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don\'t include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/plugins/android/skills/android-emulator-qa/SKILL.md b/plugins/android/skills/android-emulator-qa/SKILL.md new file mode 100644 index 00000000..d2e6e152 --- /dev/null +++ b/plugins/android/skills/android-emulator-qa/SKILL.md @@ -0,0 +1,80 @@ +--- +name: "android-emulator-qa" +description: "Use when validating Android feature flows in an emulator with adb-driven launch, input, UI-tree inspection, screenshots, and logcat capture." +--- + +# Android Emulator QA + +Validate Android app flows in an emulator using adb for launch, input, UI-tree inspection, screenshots, and logs. + +## When to use +- QA a feature flow in an Android emulator. +- Reproduce UI bugs by driving navigation with adb input events. +- Capture screenshots and logcat output while testing. + +## Quick start +1) List emulators and pick a serial: + - `adb devices` +2) Build and install the target variant: + - `./gradlew ::install --console=plain --quiet` + - If unsure about task names: `./gradlew tasks --all | rg install` +3) Launch the app: + - Resolve activity: `adb -s shell cmd package resolve-activity --brief ` + - Start app: `adb -s shell am start -n /` +4) Capture a screenshot for visual verification: + - `adb -s exec-out screencap -p > /tmp/emu.png` + +## adb control commands +- Tap (use UI tree-derived coordinates): + - `adb -s shell input tap ` +- Swipe: + - `adb -s shell input swipe ` + - Avoid edges (start ~150-200 px from left/right) to reduce accidental back gestures. +- Text: + - `adb -s shell input text "hello"` +- Back: + - `adb -s shell input keyevent 4` +- UI tree dump: + - `adb -s exec-out uiautomator dump /dev/tty` + +## Coordinate picking (UI tree only) +Always compute tap coordinates from the UI tree, not screenshots. + +1) Dump the UI tree to a step-specific file: + - `adb -s exec-out uiautomator dump /dev/tty > /tmp/ui-settings.xml` +2) Find the target node and derive center coordinates (`x y`) from bounds: + - Bounds format: `bounds="[x1,y1][x2,y2]"` + - Helper script: + - `python3 /scripts/ui_pick.py /tmp/ui-settings.xml "Settings"` +3) If the node is missing and there are `scrollable` elements: + - swipe, re-dump, and re-search at least once before concluding the target is missing. +4) Tap the center: + - `adb -s shell input tap ` + +## UI tree skeleton (helper) +Use this helper to create a compact, readable overview before inspecting full XML. + +1) Dump full UI tree: + - `adb -s exec-out uiautomator dump /dev/tty > /tmp/ui-full.xml` +2) Generate summary: + - `python3 /scripts/ui_tree_summarize.py /tmp/ui-full.xml /tmp/ui-summary.txt` +3) Review `/tmp/ui-summary.txt` to choose likely targets, then compute exact bounds from full XML. + +## Logs (logcat) +1) Clear logs: + - `adb -s logcat -c` +2) Stream app process logs: + - Resolve pid: `adb -s shell pidof -s ` + - Stream: `adb -s logcat --pid ` +3) Crash buffer only: + - `adb -s logcat -b crash` +4) Save logs: + - `adb -s logcat -d > /tmp/logcat.txt` + +## Package shortcuts +- List installed packages: + - `adb -s shell pm list packages` +- Filter to your namespace: + - `adb -s shell pm list packages | rg ` +- Confirm the activity resolves before launching: + - `adb -s shell cmd package resolve-activity --brief ` diff --git a/plugins/android/skills/android-emulator-qa/agents/openai.yaml b/plugins/android/skills/android-emulator-qa/agents/openai.yaml new file mode 100644 index 00000000..bacb6752 --- /dev/null +++ b/plugins/android/skills/android-emulator-qa/agents/openai.yaml @@ -0,0 +1,15 @@ +interface: + display_name: "Android Emulator QA" + short_description: "Drive Android emulator QA with adb" + +dependencies: + tools: + - type: "cli" + value: "adb" + description: "Android Debug Bridge for emulator/app control" + - type: "cli" + value: "python3" + description: "Run UI tree helper scripts" + - type: "cli" + value: "gradle" + description: "Build and install app variants" diff --git a/plugins/android/skills/android-emulator-qa/scripts/ui_pick.py b/plugins/android/skills/android-emulator-qa/scripts/ui_pick.py new file mode 100644 index 00000000..401a843d --- /dev/null +++ b/plugins/android/skills/android-emulator-qa/scripts/ui_pick.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +import re +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + + +def clean_text(value: str | None) -> str: + if not value: + return "" + return re.sub(r"\s+", " ", value).strip() + + +def read_trimmed_xml(path: Path) -> str: + text = path.read_text() + end = text.find("") + if end == -1: + raise ValueError("end tag not found") + return text[: end + len("")] + + +def find_node(root: ET.Element, target: str) -> ET.Element | None: + normalized_target = clean_text(target) + for element in root.iter(): + text = clean_text(element.attrib.get("text")) + desc = clean_text(element.attrib.get("content-desc")) + if text == normalized_target or desc == normalized_target: + return element + return None + + +def main() -> int: + if len(sys.argv) != 3: + print("Usage: ui_pick.py ", file=sys.stderr) + return 2 + + path = Path(sys.argv[1]) + target = sys.argv[2] + + try: + trimmed_xml = read_trimmed_xml(path) + except (OSError, ValueError) as exc: + print(f"error: {exc}", file=sys.stderr) + return 2 + + try: + root = ET.fromstring(trimmed_xml) + except ET.ParseError as exc: + print(f"error: failed to parse xml: {exc}", file=sys.stderr) + return 2 + + node = find_node(root, target) + if node is None: + print("error: node not found", file=sys.stderr) + return 2 + + bounds = node.attrib.get("bounds", "") + match = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds) + if not match: + print("error: bounds not found", file=sys.stderr) + return 2 + + x1, y1, x2, y2 = map(int, match.groups()) + x = (x1 + x2) // 2 + y = (y1 + y2) // 2 + print(f"{x} {y}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins/android/skills/android-emulator-qa/scripts/ui_tree_summarize.py b/plugins/android/skills/android-emulator-qa/scripts/ui_tree_summarize.py new file mode 100644 index 00000000..23ef643b --- /dev/null +++ b/plugins/android/skills/android-emulator-qa/scripts/ui_tree_summarize.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +import re +import sys +import xml.etree.ElementTree as ET + +INTERACTIVE_ATTRS = ("clickable", "long-clickable", "scrollable", "focusable") +STATE_ATTRS = ("checked", "selected") +DISPLAY_ATTRS = ("text", "content-desc", "resource-id") +MAX_DEPTH = 20 + + +def clean_text(value): + if not value: + return "" + return re.sub(r"\s+", " ", value).strip() + + +def is_true(value): + return value == "true" + + +def simplify_class(value): + if not value: + return "" + return value.split(".")[-1] + + +def simplify_resource_id(value): + if not value: + return "" + if ":id/" in value: + prefix, rest = value.split(":id/", 1) + if prefix and prefix != "android": + return "id/" + rest + return value + + +def extract_labels(node): + text = clean_text(node.attrib.get("text", "")) + desc = clean_text(node.attrib.get("content-desc", "")) + resource_id = simplify_resource_id(node.attrib.get("resource-id", "")) + return text, desc, resource_id + + +def is_interactive(node): + for key in INTERACTIVE_ATTRS: + if is_true(node.attrib.get(key, "false")): + return True + return False + + +def has_display(node): + text, desc, resource_id = extract_labels(node) + return bool(text or desc or resource_id) + + +def keep_node(node): + return has_display(node) or is_interactive(node) or is_true(node.attrib.get("scrollable", "false")) + + +def format_node(node): + class_name = simplify_class(node.attrib.get("class", "")) + text, desc, resource_id = extract_labels(node) + parts = [class_name] + if resource_id: + parts.append(f"id={resource_id}") + if text: + parts.append(f'text="{text}"') + if desc: + parts.append(f'desc="{desc}"') + flags = [] + for key in INTERACTIVE_ATTRS: + if is_true(node.attrib.get(key, "false")): + flags.append(key) + for key in STATE_ATTRS: + if is_true(node.attrib.get(key, "false")): + flags.append(key) + if flags: + parts.append("flags=" + ",".join(flags)) + bounds = node.attrib.get("bounds", "") + if bounds and (is_interactive(node) or has_display(node)): + parts.append(f"bounds={bounds}") + return " ".join(parts) + + +def build_lines(node, depth): + if depth > MAX_DEPTH: + return [] + include = keep_node(node) + child_depth = depth + 1 if include else depth + lines = [] + if include: + lines.append((" " * depth) + format_node(node)) + for child in node: + lines.extend(build_lines(child, child_depth)) + return lines + + +def main(): + if len(sys.argv) != 3: + raise SystemExit("usage: ui_tree_summarize.py ") + input_path = sys.argv[1] + output_path = sys.argv[2] + + with open(input_path, "r", encoding="utf-8") as handle: + xml_text = handle.read() + end_marker = "" + end_index = xml_text.rfind(end_marker) + if end_index == -1: + raise SystemExit("hierarchy end tag not found") + xml_text = xml_text[: end_index + len(end_marker)] + root = ET.fromstring(xml_text) + + lines = [] + for child in root: + lines.extend(build_lines(child, 0)) + + with open(output_path, "w", encoding="utf-8") as handle: + handle.write("\n".join(lines)) + handle.write("\n") + + +if __name__ == "__main__": + main()