Full Code of google-deepmind/android_env for AI

main 0cdf2711c4e9 cached
148 files
908.6 KB
221.9k tokens
809 symbols
1 requests
Download .txt
Showing preview only (962K chars total). Download the full file or copy to clipboard to get everything.
Repository: google-deepmind/android_env
Branch: main
Commit: 0cdf2711c4e9
Files: 148
Total size: 908.6 KB

Directory structure:
gitextract_70x6n_qo/

├── .github/
│   └── workflows/
│       └── tests.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── android_env/
│   ├── __init__.py
│   ├── apps/
│   │   ├── MODULE.bazel
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── google/
│   │   │           └── androidenv/
│   │   │               ├── accessibilityforwarder/
│   │   │               │   ├── AccessibilityForwarder.kt
│   │   │               │   ├── AccessibilityForwarderTest.kt
│   │   │               │   ├── AccessibilityTreeCreator.kt
│   │   │               │   ├── AccessibilityTreeCreatorTest.kt
│   │   │               │   ├── AndroidManifest.xml
│   │   │               │   ├── AndroidManifest_lite.xml
│   │   │               │   ├── FlagsBroadcastReceiver.kt
│   │   │               │   ├── FlagsBroadcastReceiverTest.kt
│   │   │               │   ├── LogFlags.kt
│   │   │               │   ├── ParentChildNodePair.kt
│   │   │               │   ├── UniqueIdsGenerator.kt
│   │   │               │   └── res/
│   │   │               │       └── xml/
│   │   │               │           └── accessibility_forwarder_service.xml
│   │   │               └── catch/
│   │   │                   ├── AndroidManifest.xml
│   │   │                   ├── BUILD.bazel
│   │   │                   ├── GameLogic.kt
│   │   │                   ├── GameLogicThread.kt
│   │   │                   ├── MainActivity.kt
│   │   │                   ├── RenderThread.kt
│   │   │                   ├── res/
│   │   │                   │   ├── layout/
│   │   │                   │   │   └── main.xml
│   │   │                   │   └── values/
│   │   │                   │       └── strings.xml
│   │   │                   └── sprite/
│   │   │                       ├── BUILD.bazel
│   │   │                       ├── Background.kt
│   │   │                       ├── Ball.kt
│   │   │                       ├── LineSegment.kt
│   │   │                       ├── Paddle.kt
│   │   │                       ├── Point.kt
│   │   │                       └── Sprite.kt
│   │   └── javatests/
│   │       └── com/
│   │           └── google/
│   │               └── androidenv/
│   │                   └── catch/
│   │                       ├── AndroidManifest.xml
│   │                       ├── BUILD.bazel
│   │                       ├── GameLogicTest.kt
│   │                       ├── GameLogicThreadTest.kt
│   │                       ├── MainActivityTest.kt
│   │                       ├── RenderThreadTest.kt
│   │                       └── sprite/
│   │                           ├── BUILD.bazel
│   │                           ├── BackgroundTest.kt
│   │                           ├── BallTest.kt
│   │                           ├── PaddleTest.kt
│   │                           └── SpriteTest.kt
│   ├── components/
│   │   ├── __init__.py
│   │   ├── action_fns.py
│   │   ├── action_fns_test.py
│   │   ├── action_type.py
│   │   ├── adb_call_parser.py
│   │   ├── adb_call_parser_test.py
│   │   ├── adb_controller.py
│   │   ├── adb_controller_test.py
│   │   ├── adb_log_stream.py
│   │   ├── adb_log_stream_test.py
│   │   ├── app_screen_checker.py
│   │   ├── app_screen_checker_test.py
│   │   ├── config_classes.py
│   │   ├── coordinator.py
│   │   ├── coordinator_test.py
│   │   ├── device_settings.py
│   │   ├── device_settings_test.py
│   │   ├── dumpsys_thread.py
│   │   ├── dumpsys_thread_test.py
│   │   ├── errors.py
│   │   ├── errors_test.py
│   │   ├── log_stream.py
│   │   ├── log_stream_test.py
│   │   ├── logcat_thread.py
│   │   ├── logcat_thread_test.py
│   │   ├── pixel_fns.py
│   │   ├── pixel_fns_test.py
│   │   ├── setup_step_interpreter.py
│   │   ├── setup_step_interpreter_test.py
│   │   ├── simulators/
│   │   │   ├── __init__.py
│   │   │   ├── base_simulator.py
│   │   │   ├── base_simulator_test.py
│   │   │   ├── emulator/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── emulator_launcher.py
│   │   │   │   ├── emulator_launcher_test.py
│   │   │   │   ├── emulator_simulator.py
│   │   │   │   └── emulator_simulator_test.py
│   │   │   └── fake/
│   │   │       ├── __init__.py
│   │   │       ├── fake_simulator.py
│   │   │       └── fake_simulator_test.py
│   │   ├── specs.py
│   │   ├── specs_test.py
│   │   ├── task_manager.py
│   │   └── task_manager_test.py
│   ├── env_interface.py
│   ├── environment.py
│   ├── environment_test.py
│   ├── loader.py
│   ├── loader_test.py
│   ├── proto/
│   │   ├── __init__.py
│   │   ├── a11y/
│   │   │   ├── __init__.py
│   │   │   ├── a11y.proto
│   │   │   ├── android_accessibility_action.proto
│   │   │   ├── android_accessibility_forest.proto
│   │   │   ├── android_accessibility_node_info.proto
│   │   │   ├── android_accessibility_node_info_clickable_span.proto
│   │   │   ├── android_accessibility_tree.proto
│   │   │   ├── android_accessibility_window_info.proto
│   │   │   └── rect.proto
│   │   ├── adb.proto
│   │   ├── emulator_controller.proto
│   │   ├── snapshot.proto
│   │   ├── snapshot_service.proto
│   │   ├── state.proto
│   │   └── task.proto
│   └── wrappers/
│       ├── __init__.py
│       ├── a11y/
│       │   ├── __init__.py
│       │   ├── a11y_events.py
│       │   ├── a11y_events_test.py
│       │   ├── a11y_forests.py
│       │   ├── a11y_forests_test.py
│       │   ├── a11y_servicer.py
│       │   └── a11y_servicer_test.py
│       ├── a11y_grpc_wrapper.py
│       ├── a11y_grpc_wrapper_test.py
│       ├── base_wrapper.py
│       ├── base_wrapper_test.py
│       ├── discrete_action_wrapper.py
│       ├── discrete_action_wrapper_test.py
│       ├── flat_interface_wrapper.py
│       ├── flat_interface_wrapper_test.py
│       ├── float_pixels_wrapper.py
│       ├── float_pixels_wrapper_test.py
│       ├── gym_wrapper.py
│       ├── gym_wrapper_test.py
│       ├── image_rescale_wrapper.py
│       ├── image_rescale_wrapper_test.py
│       ├── last_action_wrapper.py
│       ├── last_action_wrapper_test.py
│       ├── rate_limit_wrapper.py
│       ├── rate_limit_wrapper_test.py
│       ├── tap_action_wrapper.py
│       └── tap_action_wrapper_test.py
├── docs/
│   ├── emulator_guide.md
│   ├── environment.md
│   ├── example_tasks.md
│   ├── instructions.md
│   └── tasks_guide.md
├── examples/
│   ├── __init__.py
│   ├── run_acme_agent.py
│   ├── run_human_agent.py
│   └── run_random_agent.py
├── pyproject.toml
└── setup.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/tests.yml
================================================
name: tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  workflow_dispatch:
    inputs:
      git-ref:
        description: Git Ref (Optional)
        required: false

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      TEST_TMPDIR: '/tmp'
    strategy:
      matrix:
        python-version: ["3.11", "3.12", "3.13"]
    steps:
      - uses: actions/checkout@v6

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: |
          pip install --upgrade pip setuptools
          python setup.py install
          pip install .[testing]

      - name: Run tests
        run: |
          # Find all test files, print their names and execute them in parallel
          # with a maximum of 20 proccesses.
          find . -type f -name "*_test.py" -print0 | xargs -t -0 -n1 -P 20 python3


================================================
FILE: CONTRIBUTING.md
================================================

# How to Contribute

# Pull Requests

Please send in fixes or feature additions through Pull Requests.

## Contributor License Agreement

Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution,
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.

You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.

## Code reviews

All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.


================================================
FILE: LICENSE
================================================

                                 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.

================================================
FILE: README.md
================================================

# AndroidEnv - The Android Learning Environment

<img align="right" src="docs/images/device_control.gif" width="160" height="240">

[AndroidEnv](https://github.com/deepmind/android_env) is a Python library that
exposes an [Android](https://www.android.com/) device as a Reinforcement
Learning (RL) environment. The library provides a flexible platform for defining
custom tasks on top of the Android Operating System, including any Android
application. Agents interact with the device through a universal action
interface - the touchscreen - by sending localized touch and lift events to the
system. The library processes these events and returns pixel observations and
rewards as provided by specific [task definitions](docs/tasks_guide.md). For
example, rewards might be given for events such as successfully scrolling down a
page, sending an email, or achieving some score in a game, depending on the
research purpose and how the user configures the task.

[![tests](https://github.com/deepmind/android_env/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/deepmind/android_env/actions/workflows/tests.yml)
[![PyPI version](https://badge.fury.io/py/android-env.svg)](https://badge.fury.io/py/android-env)
[![Downloads](https://pepy.tech/badge/android-env)](https://pepy.tech/project/android-env)

## Index

*   [Environment details](docs/environment.md)
*   [Running AndroidEnv](docs/instructions.md)
*   [Setting up a virtual Android device](docs/emulator_guide.md)
*   [Defining a task in AndroidEnv](docs/tasks_guide.md)
*   [Example tasks available for download](docs/example_tasks.md)

## Environment features

There are a number of aspects that make AndroidEnv a challenging yet suitable
environment for Reinforcement Learning research:

*   Allowing agents to interact with a system used daily by billions of users
    around the world, AndroidEnv offers a platform for RL agents to navigate,
    learn tasks and have direct impact in **real-world contexts**. The
    environment wraps a simulated Android device, which runs independently from
    the environment, completely unaltered, and works in exactly the same way as
    the devices that humans use, exposing exactly the same features and
    services.

*   The platform offers a virtually infinite **range of possible tasks**, all
    sharing a common action interface. The library facilitates the design of
    Reinforcement Learning tasks for any existing or custom built Android
    application. For example, it exposes the broad world of Android games,
    ranging from card games, puzzle games, time reactive games, all requiring a
    diverse set of action combinations and interaction types.

*   The environment runs on top of a **real-time simulation** of an Android
    device. In other words, the environment dynamics does not wait for the agent
    to deliberate, and the speed of the simulation cannot be increased.

*   The observation is a collection of **RGB values** corresponding to the
    displayed pixels on the screen. The exact screen resolution depends on the
    simulated device, but in general it will be considered relatively large in
    an RL context. However, users have the option of downsampling each
    observation.

*   The learning environment has an interesting, **complex action space** unique
    to the touchscreen interface of Android.

    *   The raw, **hybrid action space** consists of a continuous tuple
        signifying the action location, and a discrete signal determining
        whether the agent wants to touch the screen or lift its virtual finger.
    *   Raw actions are highly **composable**: the Android UI and most
        applications were designed so that they could be intuitively navigated
        via common
        [touchscreen gestures](https://developer.android.com/training/gestures/detector)
        such as tapping, scrolling, swiping, pinching, drag & drop etc. This is
        still the case in AndroidEnv: to trigger meaningful changes in the
        environment, the agent often has to perform carefully timed and
        positioned sequences of raw actions. For example, in order to navigate
        to the next image in a photo gallery, the agent would have to perform a
        *swipe*, touching the screen multiple times, gradually shifting the
        actions' positions to the right. Thus, in most contexts raw actions do
        not trigger changes in the state of the environment unless correctly
        chained together to make up a human gesture.
    *   The action interface is **closely related to the observation space**, as
        meaningful touch and lift events are often either co-localized or
        strongly correlated to the location or movement of salient objects in
        the observation. For example, the position of a button on the screen
        aligns with the location of the actions that trigger the button press.
    *   The library provides tools for flexibly **altering the action
        interface** if needed for particular studies, such as discretization or
        hard-coding gesture skills. Still, we believe that the real challenge
        remains in devising agents that are capable of dealing with a large
        suite of diverse tasks, through acting and learning in the complex
        unifying action interface.

# Getting started

### Installation

The easiest way to get AndroidEnv is with pip:

```shell
$ python3 -m pip install android-env
```

Please note that `/examples` are not included in this package.

Alternatively, you can clone the repository from git's `main` branch:

```shell
$ git clone https://github.com/deepmind/android_env/
$ cd android_env
$ python3 setup.py install
```

Update: the environment now runs on Windows, but please keep in mind that this
option is not well-maintained or widely supported, as Unix-based systems are the
primary target platforms of this project.

### Create a simulator

Before running the environment, you will need access to an emulated Android
device. For instructions on creating a virtual Android device, see the
[Emulator guide](docs/emulator_guide.md).

### Define a task

Then, you will want to define what the agent's *task* is. At this point, the
agent will be able to communicate with the emulated device, but it will not yet
have an objective, or access to signals such as rewards or RL episode ends.
Learn [how to define an RL task](docs/tasks_guide.md) of your own, or use one of
the [existing task definitions](docs/example_tasks.md) for training.

### Load and run

To find out how to run and train agents on AndroidEnv, see these
[detailed instructions](docs/instructions.md). Here you can also find example
scripts demonstrating how to run a random agent, an
[acme](https://github.com/deepmind/acme) agent, or a human agent on AndroidEnv.

## About

This library is developed and maintained by [DeepMind](http://deepmind.com). \
You can find the [technical report](https://arxiv.org/abs/2105.13231) on Arxiv,
as well as an introductory
[blog
post](https://www.deepmind.com/publications/androidenv-the-android-learning-environment)
on DeepMind's website.

If you use AndroidEnv in your research, you can cite the paper using the
following BibTeX:

```
@article{ToyamaEtAl2021AndroidEnv,
  title     = {{AndroidEnv}: A Reinforcement Learning Platform for Android},
  author    = {Daniel Toyama and Philippe Hamel and Anita Gergely and
               Gheorghe Comanici and Amelia Glaese and Zafarali Ahmed and Tyler
               Jackson and Shibl Mourad and Doina Precup},
  year      = {2021},
  eprint    = {2105.13231},
  archivePrefix = {arXiv},
  primaryClass = {cs.LG},
  volume    = {abs/2105.13231},
  url       = {http://arxiv.org/abs/2105.13231},
}
```

Disclaimer: This is not an official Google product.


================================================
FILE: android_env/__init__.py
================================================
# coding=utf-8
# Copyright 2026 DeepMind Technologies Limited.
#
# 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.



================================================
FILE: android_env/apps/MODULE.bazel
================================================
# Copyright 2026 DeepMind Technologies Limited.
#
# 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.

# Bazel dependencies for building Catch.
module(
    name = "catch",
    version = "1.0",
)

bazel_dep(name = "rules_android", version = "0.6.6")
bazel_dep(name = "rules_kotlin", version = "2.1.8")
bazel_dep(name = "rules_jvm_external", version = "6.7")
bazel_dep(name = "rules_robolectric", version = "4.16", repo_name = "robolectric")
bazel_dep(name = "rules_java", version = "9.0.3")
bazel_dep(name = "protobuf", version = "30.0")

# To avoid conflict with different protobuf versions.
single_version_override(
    module_name = "protobuf",
    version = "30.0",
)

maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

# Need to set testonly = True because the package depends on testonly targets.
maven.artifact(
    testonly = True,
    artifact = "runner",
    group = "androidx.test",
    version = "1.7.0",
)
maven.artifact(
    testonly = True,
    artifact = "junit",
    group = "androidx.test.ext",
    version = "1.3.0",
)
maven.artifact(
    testonly = True,
    artifact = "mockito-kotlin",
    group = "org.mockito.kotlin",
    version = "6.1.0",
)
maven.install(
    artifacts = [
        "androidx.test.ext:junit:1.3.0",
        "androidx.test:runner:1.7.0",
        "com.google.guava:guava:32.0.1-jre",
        "com.google.truth:truth:1.4.0",
        "org.mockito.kotlin:mockito-kotlin:6.1.0",
        "org.mockito:mockito-core:5.20.0",
        "org.robolectric:robolectric:4.16",
        "org.yaml:snakeyaml:2.5",
    ],
    repositories = [
        "https://maven.google.com",
        "https://repo1.maven.org/maven2",
    ],
)
use_repo(maven, "maven")

remote_android_extensions = use_extension(
    "@rules_android//bzlmod_extensions:android_extensions.bzl",
    "remote_android_tools_extensions",
)
use_repo(remote_android_extensions, "android_tools")

android_sdk_repository_extension = use_extension("@rules_android//rules/android_sdk_repository:rule.bzl", "android_sdk_repository_extension")
use_repo(android_sdk_repository_extension, "androidsdk")


================================================
FILE: android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityForwarder.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.accessibilityforwarder

import android.accessibilityservice.AccessibilityService
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityWindowInfo
import com.google.androidenv.accessibilityforwarder.A11yServiceGrpcKt.A11yServiceCoroutineStub
import io.grpc.ManagedChannel
import io.grpc.ManagedChannelBuilder
import io.grpc.ProxyDetector
import io.grpc.StatusException
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout

/**
 * An Android service that listens to accessibility events and forwards them via gRPC.
 *
 * This service also logs the accessibility tree if [LogFlags.logAccessibilityTree] is set and if
 * [LogFlags.grpcPort] is positive.
 *
 * Please see
 * https://developer.android.com/reference/android/view/accessibility/AccessibilityEvent#getEventType()
 * for a comprehensive list of events emitted by Android.
 */
class AccessibilityForwarder(
  private val channelFactory: (host: String, port: Int) -> ManagedChannel = { host, port ->
    ManagedChannelBuilder.forAddress(host, port)
      .proxyDetector(ProxyDetector { _ -> null })
      .usePlaintext()
      .build()
  }
) : AccessibilityService() {

  init {
    // Spawn long-running thread for periodically logging the tree.
    Thread(
        Runnable {
          while (LogFlags.a11yTreePeriodMs > 0) {
            try {
              logAccessibilityTree()
            } catch (e: ConcurrentModificationException) {
              continue
            }

            Thread.sleep(/* millis= */ LogFlags.a11yTreePeriodMs)
          }
        }
      )
      .start()
  }

  // grpcStub has a backing property that can be reset to null.
  private var _grpcStub: A11yServiceCoroutineStub? = null
  val grpcStub: A11yServiceCoroutineStub
    get() {
      if (_grpcStub == null) {
        Log.i(TAG, "Building channel on ${LogFlags.grpcHost}:${LogFlags.grpcPort}.")
        _grpcStub = A11yServiceCoroutineStub(channelFactory(LogFlags.grpcHost, LogFlags.grpcPort))
      }
      return _grpcStub!!
    }

  private fun resetGrpcStub() {
    _grpcStub = null
  }

  override fun onInterrupt() {
    LogFlags.a11yTreePeriodMs = 0 // Turn off periodic tree forwarding.
  }

  override fun onAccessibilityEvent(event: AccessibilityEvent?) {
    if (event == null) {
      Log.i(TAG, "`event` is null.")
      return
    }

    logExtrasForEvent(event)
    val eventType = event.eventType
    val eventTypeStr: String = AccessibilityEvent.eventTypeToString(eventType)
    if (eventTypeStr.isNotEmpty()) {
      Log.i(TAG, eventTypeStr)
    }
  }

  private fun logAccessibilityTree() {
    if (!LogFlags.logAccessibilityTree) {
      Log.i(TAG, "Not logging accessibility tree")
      return
    }

    val windows = getWindowsOrNull()

    if (windows == null) {
      Log.i(TAG, "windows is null.")
      return
    }

    // Check gRPC port before actually building the forest.
    if (LogFlags.grpcPort <= 0) {
      Log.w(TAG, "Can't log accessibility tree because gRPC port has not been set.")
      return
    }

    val forest = creator.buildForest(windows)
    try {
      val grpcTimeoutMillis = 1000L
      val response: ForestResponse =
        with(grpcStub) {
          Log.i(TAG, "sending (blocking) gRPC request for tree.")
          runBlocking { withTimeout(grpcTimeoutMillis) { sendForest(forest) } }
        }
      if (response.error.isNotEmpty()) {
        Log.w(TAG, "gRPC response.error: ${response.error}")
      } else {
        Log.i(TAG, "gRPC request for tree succeeded.")
      }
    } catch (e: StatusException) {
      Log.w(TAG, "gRPC StatusException; are you sure networking is turned on?")
      Log.i(TAG, "extra: exception ['$e']")
      resetGrpcStub()
    } catch (e: TimeoutCancellationException) {
      Log.w(TAG, "gRPC TimeoutCancellationException; are you sure networking is turned on?")
      Log.i(TAG, "extra: exception ['$e']")
      resetGrpcStub()
    }
  }

  private fun getWindowsOrNull(): List<AccessibilityWindowInfo>? =
    try {
      windows
    } catch (e: NullPointerException) {
      null
    }

  /** Logs extras for all event types. */
  private fun logExtrasForEvent(event: AccessibilityEvent) {

    val events: MutableMap<String, String> = mutableMapOf()

    val sourceDescription = event.source?.contentDescription()
    if (!sourceDescription.isNullOrEmpty()) {
      events.put("source_content_description", sourceDescription)
    }

    // Output the event text.
    val eventText = event.text.joinToString(", ")
    if (eventText.isNotEmpty()) {
      events.put("event_text", eventText)
    }

    // Output the source text.
    val sourceText = event.source?.text?.toString()
    if (!sourceText.isNullOrEmpty()) {
      events.put("source_text", sourceText)
    }

    val eventTypeStr: String = AccessibilityEvent.eventTypeToString(event.eventType)
    if (eventTypeStr.isNotEmpty()) {
      events.put("event_type", eventTypeStr)
    }

    val className = event.source?.className?.toString()
    if (!className.isNullOrEmpty()) {
      events.put("source_class_name", className)
    }

    val packageName = event.packageName?.toString()
    if (!packageName.isNullOrEmpty()) {
      events.put("event_package_name", packageName)
    }

    // Text editing properties.
    val beforeText = event.beforeText?.toString()
    if (!beforeText.isNullOrEmpty()) {
      events.put("before_text", beforeText)
    }

    val fromIndex = event.fromIndex
    if (fromIndex != -1) {
      events.put("from_index", fromIndex.toString())
    }

    val toIndex = event.toIndex
    if (toIndex != -1) {
      events.put("to_index", toIndex.toString())
    }

    val addedCount = event.addedCount
    if (addedCount != -1) {
      events.put("added_count", addedCount.toString())
    }

    val removedCount = event.removedCount
    if (removedCount != -1) {
      events.put("removed_count", removedCount.toString())
    }

    //  Text traversal properties
    val movementGranularity = event.movementGranularity
    if (movementGranularity != 0) {
      events.put("movement_granularity", movementGranularity.toString())
    }

    val action = event.action
    if (action != 0) {
      events.put("action", action.toString())
    }

    // Scrolling properties.
    if (eventTypeStr == "TYPE_VIEW_SCROLLED") {
      events.put("scroll_delta_x", event.scrollDeltaX.toString())
      events.put("scroll_delta_y", event.scrollDeltaY.toString())
    }

    // Report viewID so we know exactly where the event came from.
    val viewId = event.source?.viewIdResourceName?.toString()
    if (!viewId.isNullOrEmpty()) {
      events.put("view_id", viewId)
    }

    // Format [events] as a Python dict.
    if (events.isNotEmpty()) {
      events.put("event_timestamp_ms", event.eventTime.toString(10))
      // Check if we want to use gRPC.
      if (LogFlags.grpcPort > 0) {
        try {
          val grpcTimeoutMillis = 1000L
          val request = eventRequest { this.event.putAll(events) }
          val response: EventResponse =
            with(grpcStub) {
              Log.i(TAG, "sending (blocking) gRPC request for event.")
              runBlocking { withTimeout(grpcTimeoutMillis) { sendEvent(request) } }
            }
          if (response.error.isNotEmpty()) {
            Log.w(TAG, "gRPC response.error: ${response.error}")
          } else {
            Log.i(TAG, "gRPC request for event succeeded.")
          }
        } catch (e: StatusException) {
          Log.w(TAG, "gRPC StatusException; are you sure networking is turned on?")
          Log.i(TAG, "extra: exception ['$e']")
          resetGrpcStub()
        } catch (e: TimeoutCancellationException) {
          Log.w(TAG, "gRPC TimeoutCancellationException; are you sure networking is turned on?")
          Log.i(TAG, "extra: exception ['$e']")
          resetGrpcStub()
        }
      } else {
        Log.w(TAG, "Can't log accessibility event because gRPC port has not been set.")
      }
    }
  }

  /** Recursively climbs the accessibility tree until the root, collecting descriptions. */
  private fun AccessibilityNodeInfo?.contentDescription(): String {
    if (this == null) {
      return ""
    }

    val descriptions = mutableListOf<String>()
    var current: AccessibilityNodeInfo? = this
    while (current != null) {
      val description = current.contentDescription
      if (description != null) {
        descriptions.add(description.toString())
      }

      current = current.parent
    }
    return descriptions.joinToString(", ")
  }

  companion object {
    private const val TAG = "AndroidRLTask"
    private val creator = AccessibilityTreeCreator()
  }
}


================================================
FILE: android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityForwarderTest.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.accessibilityforwarder

import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityWindowInfo
import com.google.common.truth.Truth.assertThat
import io.grpc.Status
import io.grpc.StatusException
import io.grpc.inprocess.InProcessChannelBuilder
import io.grpc.inprocess.InProcessServerBuilder
import io.grpc.testing.GrpcCleanupRule
import org.junit.Assert.assertFalse
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestParameterInjector
import org.robolectric.Shadows.shadowOf

@RunWith(RobolectricTestParameterInjector::class)
class AccessibilityForwarderTest {

  @get:Rule(order = 1) val cleanupRule = GrpcCleanupRule()

  class FakeAccessibilityService : A11yServiceGrpcKt.A11yServiceCoroutineImplBase() {
    var sendForestChecker: (AndroidAccessibilityForest) -> String = { _ -> "" }
    var sendEventChecker: (EventRequest) -> String = { _ -> "" }

    override suspend fun sendForest(request: AndroidAccessibilityForest) = forestResponse {
      error = sendForestChecker(request)
    }

    override suspend fun sendEvent(request: EventRequest) = eventResponse {
      error = sendEventChecker(request)
    }
  }

  protected lateinit var forwarder: AccessibilityForwarder
  protected val fakeA11yService = FakeAccessibilityService()
  protected val channel by lazy {
    val serverName: String = InProcessServerBuilder.generateName()
    cleanupRule.register(
      InProcessServerBuilder.forName(serverName)
        .directExecutor()
        .addService(fakeA11yService)
        .build()
        .start()
    )
    cleanupRule.register(InProcessChannelBuilder.forName(serverName).directExecutor().build())
  }

  /** Initializes [forwarder] and [LogFlags] from the given args. */
  fun createForwarder(
    logAccessibilityTree: Boolean = false,
    a11yTreePeriodMs: Long = 0,
    grpcHost: String = "10.0.2.2",
    grpcPort: Int = 0,
    a11yWindows: MutableList<AccessibilityWindowInfo>? = null,
  ) {
    LogFlags.logAccessibilityTree = logAccessibilityTree
    LogFlags.a11yTreePeriodMs = a11yTreePeriodMs
    LogFlags.grpcHost = grpcHost
    LogFlags.grpcPort = grpcPort
    forwarder = AccessibilityForwarder({ _, _ -> channel })
    if (a11yWindows == null) {
      shadowOf(forwarder).setWindows(mutableListOf(AccessibilityWindowInfo.obtain()))
    } else {
      shadowOf(forwarder).setWindows(a11yWindows)
    }
  }

  @Test
  fun onInterrupt_doesNotCrash() {
    // Arrange.
    createForwarder(logAccessibilityTree = false)
    fakeA11yService.sendEventChecker = { _: EventRequest ->
      assertFalse(true) // This should not be called.
      "" // This should be unreachable
    }

    // Act.
    forwarder.onInterrupt()

    // Assert.
    // See `sendEventChecker` above.
  }

  @Test
  fun onAccessibilityEvent_nullEventShouldBeIgnored() {
    // Arrange.
    createForwarder(logAccessibilityTree = false)
    fakeA11yService.sendEventChecker = { _: EventRequest ->
      assertFalse(true) // This should not be called.
      "" // This should be unreachable
    }

    // Act.
    forwarder.onAccessibilityEvent(null)

    // Assert.
    // See `sendEventChecker` above.
  }

  @Test
  fun onAccessibilityEvent_knownEventWithNoInformationShouldNotBeEmitted() {
    // Arrange.
    createForwarder(logAccessibilityTree = false)
    var nodeInfo = AccessibilityNodeInfo()
    nodeInfo.setContentDescription("")
    var event = AccessibilityEvent()
    shadowOf(event).setSourceNode(nodeInfo)
    fakeA11yService.sendEventChecker = { _: EventRequest ->
      assertFalse(true) // This should not be called.
      "" // This should be unreachable
    }

    // Act.
    forwarder.onAccessibilityEvent(event)

    // Assert.
    // See `sendEventChecker` above.
  }

  @Test
  fun onAccessibilityEvent_typeViewClicked_sendEventViaGrpc() {
    // Arrange.
    createForwarder(logAccessibilityTree = false, grpcPort = 1234)
    forwarder = AccessibilityForwarder({ _, _ -> channel })
    var nodeInfo = AccessibilityNodeInfo()
    nodeInfo.setContentDescription("My Content Description")
    nodeInfo.setText("My Source Text")
    nodeInfo.setClassName("AwesomeClass")
    var event = AccessibilityEvent()
    event.setEventTime(1357924680)
    event.setEventType(AccessibilityEvent.TYPE_VIEW_CLICKED)
    event.getText().add("Some text!")
    event.setPackageName("some.loooong.package.name")
    shadowOf(event).setSourceNode(nodeInfo)
    fakeA11yService.sendEventChecker = { request: EventRequest ->
      // Check that all fields are consistent with how they were set above.
      assertThat(request.eventMap.get("event_type")).isEqualTo("TYPE_VIEW_CLICKED")
      assertThat(request.eventMap.get("event_package_name")).isEqualTo("some.loooong.package.name")
      assertThat(request.eventMap.get("source_content_description"))
        .isEqualTo("My Content Description")
      assertThat(request.eventMap.get("source_text")).isEqualTo("My Source Text")
      assertThat(request.eventMap.get("source_class_name")).isEqualTo("AwesomeClass")
      assertThat(request.eventMap.get("event_text")).isEqualTo("Some text!")
      assertThat(request.eventMap.get("event_timestamp_ms")).isEqualTo("1357924680")
      // No error message
      ""
    }

    // Act.
    forwarder.onAccessibilityEvent(event)

    // Assert.
    // See `sendEventChecker` above.
  }

  @Test
  fun onAccessibilityEvent_typeViewTextChanged_ensureAllFieldsForwarded() {
    // Arrange.
    createForwarder(logAccessibilityTree = false, grpcPort = 1234)
    var nodeInfo = AccessibilityNodeInfo()
    nodeInfo.setContentDescription("My Content Description")
    nodeInfo.setText("My Source Text")
    nodeInfo.setClassName("AwesomeClass")
    var event = AccessibilityEvent()
    event.setEventTime(1357924680)
    event.setEventType(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED)
    event.getText().add("Some text!")
    event.fromIndex = 7
    event.beforeText = "Old words"
    event.addedCount = 12
    event.removedCount = 9
    event.setPackageName("some.loooong.package.name")
    shadowOf(event).setSourceNode(nodeInfo)
    fakeA11yService.sendEventChecker = { request: EventRequest ->
      // Check that all fields are consistent with how they were set above.
      assertThat(request.eventMap.get("event_type")).isEqualTo("TYPE_VIEW_TEXT_CHANGED")
      assertThat(request.eventMap.get("event_package_name")).isEqualTo("some.loooong.package.name")
      assertThat(request.eventMap.get("source_content_description"))
        .isEqualTo("My Content Description")
      assertThat(request.eventMap.get("source_text")).isEqualTo("My Source Text")
      assertThat(request.eventMap.get("source_class_name")).isEqualTo("AwesomeClass")
      assertThat(request.eventMap.get("event_text")).isEqualTo("Some text!")
      assertThat(request.eventMap.get("event_timestamp_ms")).isEqualTo("1357924680")
      assertThat(request.eventMap.get("from_index")).isEqualTo("7")
      assertThat(request.eventMap.get("before_text")).isEqualTo("Old words")
      assertThat(request.eventMap.get("added_count")).isEqualTo("12")
      assertThat(request.eventMap.get("removed_count")).isEqualTo("9")
      assertFalse(request.eventMap.containsKey("to_index"))
      assertFalse(request.eventMap.containsKey("view_id"))
      assertFalse(request.eventMap.containsKey("action"))
      assertFalse(request.eventMap.containsKey("movement_granularity"))
      assertFalse(request.eventMap.containsKey("scroll_delta_x"))
      assertFalse(request.eventMap.containsKey("scroll_delta_y"))
      // No error message
      ""
    }

    // Act.
    forwarder.onAccessibilityEvent(event)

    // Assert.
    // See `sendEventChecker` above.
  }

  @Test
  fun onAccessibilityEvent_typeViewScrolled_ensureAllFieldsForwarded() {
    // Arrange.
    createForwarder(logAccessibilityTree = false, grpcPort = 1234)
    var nodeInfo = AccessibilityNodeInfo()
    nodeInfo.setContentDescription("My Content Description")
    nodeInfo.setText("My Source Text")
    nodeInfo.setClassName("AwesomeClass")
    var event = AccessibilityEvent()
    event.setEventTime(1357924680)
    event.setEventType(AccessibilityEvent.TYPE_VIEW_SCROLLED)
    event.getText().add("Some text!")
    event.scrollDeltaX = 13
    event.scrollDeltaY = 27
    event.setPackageName("some.loooong.package.name")
    shadowOf(event).setSourceNode(nodeInfo)
    fakeA11yService.sendEventChecker = { request: EventRequest ->
      // Check that all fields are consistent with how they were set above.
      assertThat(request.eventMap.get("event_type")).isEqualTo("TYPE_VIEW_SCROLLED")
      assertThat(request.eventMap.get("event_package_name")).isEqualTo("some.loooong.package.name")
      assertThat(request.eventMap.get("source_content_description"))
        .isEqualTo("My Content Description")
      assertThat(request.eventMap.get("source_text")).isEqualTo("My Source Text")
      assertThat(request.eventMap.get("source_class_name")).isEqualTo("AwesomeClass")
      assertThat(request.eventMap.get("event_text")).isEqualTo("Some text!")
      assertThat(request.eventMap.get("event_timestamp_ms")).isEqualTo("1357924680")
      assertThat(request.eventMap.get("scroll_delta_x")).isEqualTo("13")
      assertThat(request.eventMap.get("scroll_delta_y")).isEqualTo("27")
      assertFalse(request.eventMap.containsKey("from_index"))
      assertFalse(request.eventMap.containsKey("to_index"))
      assertFalse(request.eventMap.containsKey("before_text"))
      assertFalse(request.eventMap.containsKey("added_count"))
      assertFalse(request.eventMap.containsKey("removed_count"))
      // No error message
      ""
    }

    // Act.
    forwarder.onAccessibilityEvent(event)

    // Assert.
    // See `sendEventChecker` above.
  }

  @Test
  fun onAccessibilityEvent_typeViewTextTraversedAtMovementGranularity_ensureAllFieldsForwarded() {
    // Arrange.
    createForwarder(logAccessibilityTree = false, grpcPort = 1234)
    var nodeInfo = AccessibilityNodeInfo()
    nodeInfo.setContentDescription("My Content Description")
    nodeInfo.setText("My Source Text")
    nodeInfo.setClassName("AwesomeClass")
    nodeInfo.viewIdResourceName = "this.big.old.view.id"
    var event = AccessibilityEvent()
    event.setEventTime(1357924680)
    event.setEventType(AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY)
    event.getText().add("Some text!")
    event.setPackageName("some.loooong.package.name")
    event.movementGranularity = 5
    event.fromIndex = 6
    event.toIndex = 8
    event.action = 23
    shadowOf(event).setSourceNode(nodeInfo)
    fakeA11yService.sendEventChecker = { request: EventRequest ->
      // Check that all fields are consistent with how they were set above.
      assertThat(request.eventMap.get("event_type"))
        .isEqualTo("TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY")
      assertThat(request.eventMap.get("event_package_name")).isEqualTo("some.loooong.package.name")
      assertThat(request.eventMap.get("source_content_description"))
        .isEqualTo("My Content Description")
      assertThat(request.eventMap.get("source_text")).isEqualTo("My Source Text")
      assertThat(request.eventMap.get("source_class_name")).isEqualTo("AwesomeClass")
      assertThat(request.eventMap.get("event_text")).isEqualTo("Some text!")
      assertThat(request.eventMap.get("event_timestamp_ms")).isEqualTo("1357924680")
      assertThat(request.eventMap.get("movement_granularity")).isEqualTo("5")
      assertThat(request.eventMap.get("from_index")).isEqualTo("6")
      assertThat(request.eventMap.get("to_index")).isEqualTo("8")
      assertThat(request.eventMap.get("view_id")).isEqualTo("this.big.old.view.id")
      assertThat(request.eventMap.get("action")).isEqualTo("23")
      // No error message
      ""
    }

    // Act.
    forwarder.onAccessibilityEvent(event)

    // Assert.
    // See `sendEventChecker` above.
  }

  @Test
  fun onAccessibilityEvent_sendingevent_grpcTimeout() {
    // Arrange.
    createForwarder(
      logAccessibilityTree = false,
      a11yTreePeriodMs = 0,
      grpcHost = "amazing.host",
      grpcPort = 4321,
    )
    var nodeInfo = AccessibilityNodeInfo()
    nodeInfo.setContentDescription("My Content Description")
    nodeInfo.setText("My Source Text")
    nodeInfo.setClassName("AwesomeClass")
    var event = AccessibilityEvent()
    event.setEventTime(1357924680)
    event.setEventType(AccessibilityEvent.TYPE_VIEW_CLICKED)
    event.getText().add("Some text!")
    event.setPackageName("some.loooong.package.name")
    shadowOf(event).setSourceNode(nodeInfo)
    fakeA11yService.sendEventChecker = { _ ->
      // Delay the request to prompt a timeout
      Thread.sleep(1500L)
      "" // Return no error.
    }

    // Act.
    forwarder.onAccessibilityEvent(event)

    // Run a second request to ensure that the channel gets rebuilt.
    fakeA11yService.sendEventChecker = { _ -> "" }
    forwarder.onAccessibilityEvent(event)

    // Assert.
    // See `sendEventChecker` above.
  }

  @Test
  fun onAccessibilityEvent_sendingevent_grpcStatusException() {
    // Arrange.
    createForwarder(logAccessibilityTree = false, grpcHost = "amazing.host", grpcPort = 4321)
    var nodeInfo = AccessibilityNodeInfo()
    nodeInfo.setContentDescription("My Content Description")
    nodeInfo.setText("My Source Text")
    nodeInfo.setClassName("AwesomeClass")
    var event = AccessibilityEvent()
    event.setEventTime(1357924680)
    event.setEventType(AccessibilityEvent.TYPE_VIEW_CLICKED)
    event.getText().add("Some text!")
    event.setPackageName("some.loooong.package.name")
    shadowOf(event).setSourceNode(nodeInfo)
    fakeA11yService.sendEventChecker = { _ -> throw StatusException(Status.UNAVAILABLE) }

    // Act.
    forwarder.onAccessibilityEvent(event)

    // Run a second request to ensure that the channel gets rebuilt.
    fakeA11yService.sendEventChecker = { _ -> "" }
    forwarder.onAccessibilityEvent(event)

    // Assert.
    // See `sendEventChecker` above.
  }

  @Test
  fun logAccessibilityTreeFalse_doesNotLogAccessibilityTree() {
    // Arrange.
    createForwarder(logAccessibilityTree = false, a11yTreePeriodMs = 10, grpcPort = 13579)
    fakeA11yService.sendForestChecker = { _: AndroidAccessibilityForest ->
      assertFalse(true) // This should not be called.
      "" // This should be unreachable
    }

    // Act.
    Thread.sleep(1000) // Sleep a bit to give time to trigger the tree logging function.

    // Assert.
    // See `sendForestChecker` above.
  }

  @Test
  fun grpcPortZero_doesNotSendTree() {
    // Arrange.
    createForwarder(logAccessibilityTree = true, a11yTreePeriodMs = 10, grpcPort = 0)
    fakeA11yService.sendForestChecker = { _: AndroidAccessibilityForest ->
      assertFalse(true) // This should not be called.
      "" // This should be unreachable
    }

    // Act.
    Thread.sleep(1000) // Sleep a bit to give time to trigger the tree logging function.

    // Assert.
    // See `sendForestChecker` above.
  }

  @Test
  fun grpcPortPositive_shouldSendTreeViaGrpc() {
    // Arrange.
    val window = AccessibilityWindowInfo()
    shadowOf(window).setType(AccessibilityWindowInfo.TYPE_SYSTEM)
    createForwarder(
      logAccessibilityTree = true,
      a11yTreePeriodMs = 10,
      grpcPort = 1234,
      a11yWindows = mutableListOf(window),
    )
    fakeA11yService.sendForestChecker = { request: AndroidAccessibilityForest ->
      // Check that we get only a single window.
      assertThat(request.windowsList.size).isEqualTo(1)
      // And that its type is what we set above.
      assertThat(request.windowsList[0].windowType)
        .isEqualTo(AndroidAccessibilityWindowInfo.WindowType.TYPE_SYSTEM)
      // The error message
      "Something went wrong!"
    }

    // Act.
    Thread.sleep(1000) // Sleep a bit to give time to trigger the tree logging function.

    // Assert.
    // See `sendForestChecker` above.
  }

  @Test
  fun grpcPortPositiveAndHost_shouldSendTreeViaGrpc() {
    // Arrange.
    fakeA11yService.sendForestChecker = { request: AndroidAccessibilityForest ->
      // Check that we get only a single window.
      assertThat(request.windowsList.size).isEqualTo(1)
      // And that its type is what we set above.
      assertThat(request.windowsList[0].windowType)
        .isEqualTo(AndroidAccessibilityWindowInfo.WindowType.TYPE_ACCESSIBILITY_OVERLAY)
      "" // Return no error.
    }
    val window = AccessibilityWindowInfo()
    shadowOf(window).setType(AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY)
    createForwarder(
      logAccessibilityTree = true,
      a11yTreePeriodMs = 500,
      grpcHost = "amazing.host",
      grpcPort = 4321,
      a11yWindows = mutableListOf(window),
    )

    // Act.
    Thread.sleep(1000) // Sleep a bit to give time to trigger the tree logging function.

    // Assert.
    // See `sendForestChecker` above.
  }

  @Test
  fun sendingForest_grpcTimeout() {
    // Arrange.
    fakeA11yService.sendForestChecker = { _ ->
      // Delay the request to prompt a timeout
      Thread.sleep(1500L)
      "" // Return no error.
    }
    val window = AccessibilityWindowInfo()
    shadowOf(window).setType(AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY)
    createForwarder(
      logAccessibilityTree = true,
      a11yTreePeriodMs = 10,
      grpcHost = "amazing.host",
      grpcPort = 4321,
      a11yWindows = mutableListOf(window),
    )

    // Act.
    Thread.sleep(2000) // Sleep a bit to give time to trigger the tree logging function.

    // Run a second request to ensure that the channel gets rebuilt.
    fakeA11yService.sendForestChecker = { _ -> "" }
    Thread.sleep(2000) // Sleep a bit to give time to trigger the tree logging function.

    // Assert.
    // See `sendForestChecker` above.
  }

  @Test
  fun sendingForest_grpcStatusException() {
    // Arrange.
    val window = AccessibilityWindowInfo()
    shadowOf(window).setType(AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY)
    createForwarder(
      logAccessibilityTree = true,
      a11yTreePeriodMs = 10,
      grpcHost = "amazing.host",
      grpcPort = 4321,
      a11yWindows = mutableListOf(window),
    )
    fakeA11yService.sendForestChecker = { _ -> throw StatusException(Status.UNAVAILABLE) }

    // Act.
    Thread.sleep(1000) // Sleep a bit to give time to trigger the tree logging function.

    // Run a second request to ensure that the channel gets rebuilt.
    fakeA11yService.sendForestChecker = { _ -> "" }
    Thread.sleep(1000) // Sleep a bit to give time to trigger the tree logging function.

    // Assert.
    // See `sendForestChecker` above.
  }
}


================================================
FILE: android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityTreeCreator.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.accessibilityforwarder

import android.graphics.Rect
import android.util.Log
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityWindowInfo
import com.google.androidenv.accessibilityforwarder.AndroidAccessibilityWindowInfo.WindowType
import java.util.concurrent.ConcurrentHashMap
import java.util.stream.Collectors
import kotlin.collections.mutableListOf
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking

/** Helper methods for creating the android accessibility info extra. */
class AccessibilityTreeCreator() {

  /** Creates an accessibility forest proto. */
  fun buildForest(windowInfos: List<AccessibilityWindowInfo>): AndroidAccessibilityForest {
    val sourcesMap: ConcurrentHashMap<AndroidAccessibilityNodeInfo, AccessibilityNodeInfo> =
      ConcurrentHashMap<AndroidAccessibilityNodeInfo, AccessibilityNodeInfo>()
    val windows: List<AndroidAccessibilityWindowInfo> =
      processWindowsAndBlock(windowInfos, sourcesMap)
    return androidAccessibilityForest { this.windows += windows }
  }

  private fun processWindowsAndBlock(
    windowInfos: List<AccessibilityWindowInfo>,
    sourcesMap: ConcurrentHashMap<AndroidAccessibilityNodeInfo, AccessibilityNodeInfo>,
  ): List<AndroidAccessibilityWindowInfo> {
    val windows: List<AndroidAccessibilityWindowInfo>
    runBlocking { windows = processWindows(windowInfos, sourcesMap) }
    return windows
  }

  private suspend fun processWindows(
    windowInfos: List<AccessibilityWindowInfo>,
    sourcesMap: ConcurrentHashMap<AndroidAccessibilityNodeInfo, AccessibilityNodeInfo>,
  ): List<AndroidAccessibilityWindowInfo> {
    var windowInfoProtos = mutableListOf<AndroidAccessibilityWindowInfo>()
    for (i in windowInfos.size - 1 downTo 0) {
      val windowInfoProto = processWindow(windowInfos.get(i), sourcesMap)
      windowInfoProto?.let { windowInfoProtos.add(windowInfoProto) }
    }
    return windowInfoProtos.toList()
  }

  private suspend fun processWindow(
    windowInfo: AccessibilityWindowInfo,
    sources: ConcurrentHashMap<AndroidAccessibilityNodeInfo, AccessibilityNodeInfo>,
  ): AndroidAccessibilityWindowInfo? {
    val bounds = Rect()
    windowInfo.getBoundsInScreen(bounds)
    val root: AccessibilityNodeInfo? = windowInfo.root
    if (root == null) {
      Log.i(TAG, "window root is null")
      return androidAccessibilityWindowInfo {
        this.tree = androidAccessibilityTree {}
        this.isActive = windowInfo.isActive
        this.id = windowInfo.id
        this.layer = windowInfo.layer
        this.isAccessibilityFocused = windowInfo.isAccessibilityFocused
        this.isFocused = windowInfo.isFocused
        this.boundsInScreen = convertToRectProto(bounds)
        this.windowType = toWindowType(windowInfo.type)
      }
    }
    val treeDeferred: Deferred<AndroidAccessibilityTree>
    runBlocking { treeDeferred = async { processNodesInWindow(root, sources) } }
    return androidAccessibilityWindowInfo {
      this.tree = treeDeferred.await()
      this.isActive = windowInfo.isActive
      this.id = windowInfo.id
      this.layer = windowInfo.layer
      this.isAccessibilityFocused = windowInfo.isAccessibilityFocused
      this.isFocused = windowInfo.isFocused
      this.boundsInScreen = convertToRectProto(bounds)
      this.windowType = toWindowType(windowInfo.type)
    }
  }

  private suspend fun processNodesInWindow(
    root: AccessibilityNodeInfo,
    sources: ConcurrentHashMap<AndroidAccessibilityNodeInfo, AccessibilityNodeInfo>,
  ): AndroidAccessibilityTree {
    Log.d(TAG, "processNodesInWindow()")
    val traversalQueue = ArrayDeque<ParentChildNodePair>()
    traversalQueue.add(ParentChildNodePair.builder().child(root).build())
    val uniqueIdsCache: UniqueIdsGenerator<AccessibilityNodeInfo> = UniqueIdsGenerator()
    var currentDepth = 0
    val nodesDeferred = mutableListOf<Deferred<AndroidAccessibilityNodeInfo>>()
    val seenNodes: HashSet<AccessibilityNodeInfo> = HashSet()
    seenNodes.add(root)
    runBlocking {
      while (!traversalQueue.isEmpty()) {
        // Traverse the tree layer-by-layer.
        // The first layer has only the root and depth 0.
        // The second layer has all the root's children and depth 1.
        for (nodesAtCurrentDepth in traversalQueue.size downTo 1) {
          val nodePair: ParentChildNodePair = traversalQueue.removeFirst()
          for (i in 0 until nodePair.child().childCount) {
            val childNode: AccessibilityNodeInfo? = nodePair.child().getChild(i)
            if (childNode != null && !seenNodes.contains(childNode)) {
              traversalQueue.add(
                ParentChildNodePair.builder().child(childNode).parent(nodePair.child()).build()
              )
              seenNodes.add(childNode)
            }
          }
          val thisDepth = currentDepth
          var deferred = async { processNode(nodePair, sources, uniqueIdsCache, thisDepth) }
          nodesDeferred.add(deferred)
        }
        currentDepth++
      }
    }
    return androidAccessibilityTree { this.nodes += nodesDeferred.awaitAll() }
  }

  companion object {
    private const val TAG = "AndroidRLTask"
  }
}

private fun processNode(
  nodePair: ParentChildNodePair,
  sourceBuilder: ConcurrentHashMap<AndroidAccessibilityNodeInfo, AccessibilityNodeInfo>,
  uniqueIdsCache: UniqueIdsGenerator<AccessibilityNodeInfo>,
  nodeDepth: Int,
): AndroidAccessibilityNodeInfo {
  val node: AccessibilityNodeInfo = nodePair.child()
  val immutableNode: AndroidAccessibilityNodeInfo =
    createAndroidAccessibilityNode(
      node,
      uniqueIdsCache.getUniqueId(node),
      nodeDepth,
      getChildUniqueIds(node, uniqueIdsCache),
    )
  sourceBuilder.put(immutableNode, node)
  return immutableNode
}

private fun createAndroidAccessibilityNode(
  node: AccessibilityNodeInfo,
  nodeId: Int,
  depth: Int,
  childIds: List<Int>,
): AndroidAccessibilityNodeInfo {
  val bounds = Rect()
  node.getBoundsInScreen(bounds)
  val actions = node.getActionList().stream().map(::createAction).collect(Collectors.toList())
  return androidAccessibilityNodeInfo {
    this.actions += actions
    this.boundsInScreen = convertToRectProto(bounds)
    this.isCheckable = node.isCheckable
    this.isChecked = node.isChecked
    this.className = stringFromNullableCharSequence(node.getClassName())
    this.isClickable = node.isClickable
    this.contentDescription = stringFromNullableCharSequence(node.getContentDescription())
    this.isEditable = node.isEditable
    this.isEnabled = node.isEnabled
    this.isFocusable = node.isFocusable
    this.hintText = stringFromNullableCharSequence(node.getHintText())
    this.isLongClickable = node.isLongClickable
    this.packageName = stringFromNullableCharSequence(node.getPackageName())
    this.isPassword = node.isPassword
    this.isScrollable = node.isScrollable
    this.isSelected = node.isSelected
    this.text = stringFromNullableCharSequence(node.getText())
    this.textSelectionEnd = node.getTextSelectionEnd().toLong()
    this.textSelectionStart = node.getTextSelectionStart().toLong()
    this.viewIdResourceName = node.getViewIdResourceName() ?: ""
    this.isVisibleToUser = node.isVisibleToUser
    this.windowId = node.windowId
    this.uniqueId = nodeId
    this.childIds += childIds
    this.drawingOrder = node.drawingOrder
    this.tooltipText = stringFromNullableCharSequence(node.getTooltipText())
    this.depth = depth
  }
}

private fun createAction(
  action: AccessibilityNodeInfo.AccessibilityAction
): AndroidAccessibilityAction =
  AndroidAccessibilityAction.newBuilder()
    .setId(action.id)
    .setLabel(stringFromNullableCharSequence(action.label))
    .build()

private fun getChildUniqueIds(
  node: AccessibilityNodeInfo,
  uniqueIdsCache: UniqueIdsGenerator<AccessibilityNodeInfo>,
): List<Int> {
  val ids = mutableListOf<Int>()
  for (childId in 0 until node.getChildCount()) {
    val child: AccessibilityNodeInfo = node.getChild(childId) ?: continue
    ids.add(uniqueIdsCache.getUniqueId(child))
  }
  return ids.toList()
}

fun stringFromNullableCharSequence(cs: CharSequence?): String = cs?.toString() ?: ""

fun convertToRectProto(rect: Rect) = protoRect {
  left = rect.left
  top = rect.top
  right = rect.right
  bottom = rect.bottom
}

private fun toWindowType(type: Int): WindowType =
  when (type) {
    AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY -> WindowType.TYPE_ACCESSIBILITY_OVERLAY
    AccessibilityWindowInfo.TYPE_APPLICATION -> WindowType.TYPE_APPLICATION
    AccessibilityWindowInfo.TYPE_INPUT_METHOD -> WindowType.TYPE_INPUT_METHOD
    AccessibilityWindowInfo.TYPE_SYSTEM -> WindowType.TYPE_SYSTEM
    AccessibilityWindowInfo.TYPE_SPLIT_SCREEN_DIVIDER -> WindowType.TYPE_SPLIT_SCREEN_DIVIDER
    else -> WindowType.UNKNOWN_TYPE
  }


================================================
FILE: android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityTreeCreatorTest.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.accessibilityforwarder

import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityWindowInfo
import kotlin.test.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf

@RunWith(RobolectricTestRunner::class)
class AccessibilityTreeCreatorTest {

  @Test
  fun buildForest_buildsAccessibilityForestCorrectly() {
    val creator = AccessibilityTreeCreator()

    val forest = creator.buildForest(mutableListOf(createWindowInfo()))

    assertEquals(forest.windowsCount, 1)
    assertEquals(forest.getWindows(0).tree.nodesCount, 3)
    var rootNode: AndroidAccessibilityNodeInfo? = null
    var checkableNode: AndroidAccessibilityNodeInfo? = null
    val nodes = forest.getWindows(0).tree.nodesList
    for (i in nodes.size - 1 downTo 0) {
      if (nodes[i].text == "root node") {
        rootNode = nodes[i]
      }
      if (nodes[i].isCheckable == true) {
        checkableNode = nodes[i]
      }
    }
    assertEquals(rootNode?.childIdsCount, 2)
    assertEquals(checkableNode?.text, "Check box")
  }

  @Test
  fun buildForest_noRootInWindow_returnsEmptyTree() {
    val creator = AccessibilityTreeCreator()
    val windowInfo = AccessibilityWindowInfo.obtain()
    shadowOf(windowInfo).setType(AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY)

    val forest = creator.buildForest(mutableListOf(windowInfo))

    assertEquals(0, forest.getWindows(0).tree.nodesList.size)
  }

  private fun createAccessibilityNodeInfo(): AccessibilityNodeInfo {
    val root = AccessibilityNodeInfo.obtain()
    root.text = "root node"
    root.isClickable = true
    val accessibilityNodeInfo = AccessibilityNodeInfo.obtain()
    accessibilityNodeInfo.viewIdResourceName = "test"
    accessibilityNodeInfo.isClickable = true
    accessibilityNodeInfo.isEditable = true
    accessibilityNodeInfo.hintText = "Please enter your address"
    shadowOf(root).addChild(accessibilityNodeInfo)
    val anotherChildNode = AccessibilityNodeInfo.obtain()
    anotherChildNode.isCheckable = true
    anotherChildNode.text = "Check box"
    shadowOf(root).addChild(anotherChildNode)
    return root
  }

  private fun createWindowInfo(): AccessibilityWindowInfo {
    val windowInfo = AccessibilityWindowInfo.obtain()
    shadowOf(windowInfo).setType(AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY)
    shadowOf(windowInfo).setRoot(createAccessibilityNodeInfo())
    return windowInfo
  }
}


================================================
FILE: android_env/apps/java/com/google/androidenv/accessibilityforwarder/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2026 DeepMind Technologies Limited.

 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.-->



<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.google.androidenv.accessibilityforwarder">

  <uses-sdk
      android:minSdkVersion="28"
      android:targetSdkVersion="36" />

  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

  <application>
    <service
        android:name=".AccessibilityForwarder"
        android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
        android:exported="false">
      <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
      </intent-filter>
      <meta-data
          android:name="android.accessibilityservice"
          android:resource="@xml/accessibility_forwarder_service" />
    </service>
    <receiver android:name="com.google.androidenv.accessibilityforwarder.FlagsBroadcastReceiver" android:exported="true">
      <intent-filter>
        <action android:name="accessibility_forwarder.intent.action.ENABLE_ACCESSIBILITY_TREE_LOGS"/>
        <action android:name="accessibility_forwarder.intent.action.DISABLE_ACCESSIBILITY_TREE_LOGS"/>
        <action android:name="accessibility_forwarder.intent.action.SET_GRPC"/>
      </intent-filter>
    </receiver>
  </application>
</manifest>


================================================
FILE: android_env/apps/java/com/google/androidenv/accessibilityforwarder/AndroidManifest_lite.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2026 DeepMind Technologies Limited.

 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.-->



<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.google.androidenv.accessibilityforwarder">

  <uses-sdk
      android:minSdkVersion="28"
      android:targetSdkVersion="36" />
</manifest>


================================================
FILE: android_env/apps/java/com/google/androidenv/accessibilityforwarder/FlagsBroadcastReceiver.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.accessibilityforwarder

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log

/** Broadcast receiver responsible for enabling or disabling flags. */
class FlagsBroadcastReceiver() : BroadcastReceiver() {

  override fun onReceive(context: Context?, intent: Intent?) {
    val action = intent?.action
    Log.i(TAG, "Received broadcast intent with action: " + action)
    when (action) {
      ACTION_ENABLE_ACCESSIBILITY_TREE_LOGS -> {
        Log.i(TAG, "Enabling Accessibility Tree logging.")
        LogFlags.logAccessibilityTree = true
      }
      ACTION_DISABLE_ACCESSIBILITY_TREE_LOGS -> {
        Log.i(TAG, "Disabling Accessibility Tree logging.")
        LogFlags.logAccessibilityTree = false
      }
      ACTION_SET_GRPC -> {
        // The Android Emulator uses 10.0.2.2 as a redirect to the workstation's IP. Most often the
        // gRPC server will be running locally so it makes sense to use this as the default value.
        // See https://developer.android.com/studio/run/emulator-networking#networkaddresses.
        val host = intent.getStringExtra("host") ?: "10.0.2.2"
        // The TCP port to connect. If <=0 gRPC is disabled.
        val port = intent.getIntExtra("port", 0)
        Log.i(TAG, "Setting gRPC endpoint to ${host}:${port}.")
        LogFlags.grpcHost = host
        LogFlags.grpcPort = port
      }
      else -> Log.w(TAG, "Unknown action: ${action}")
    }
  }

  companion object {
    private const val TAG = "FlagsBroadcastReceiver"
    private const val ACTION_ENABLE_ACCESSIBILITY_TREE_LOGS =
      "accessibility_forwarder.intent.action.ENABLE_ACCESSIBILITY_TREE_LOGS"
    private const val ACTION_DISABLE_ACCESSIBILITY_TREE_LOGS =
      "accessibility_forwarder.intent.action.DISABLE_ACCESSIBILITY_TREE_LOGS"
    private const val ACTION_SET_GRPC = "accessibility_forwarder.intent.action.SET_GRPC"
  }
}


================================================
FILE: android_env/apps/java/com/google/androidenv/accessibilityforwarder/FlagsBroadcastReceiverTest.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.accessibilityforwarder

import android.content.Intent
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class FlagsBroadcastReceiverTest {

  @Test
  fun onReceive_nullIntent_shouldNotLogAnything() {
    // Arrange.
    LogFlags.logAccessibilityTree = false
    val receiver = FlagsBroadcastReceiver()

    // Act.
    receiver.onReceive(context = null, intent = null)

    // Assert.
    assertThat(LogFlags.logAccessibilityTree).isFalse()
  }

  @Test
  fun onReceive_nullIntent_actionShouldNotLogAnything() {
    // Arrange.
    LogFlags.logAccessibilityTree = false
    val receiver = FlagsBroadcastReceiver()
    val intent = Intent()

    // Act.
    receiver.onReceive(context = null, intent = intent)

    // Assert.
    assertThat(LogFlags.logAccessibilityTree).isFalse()
  }

  @Test
  fun onReceive_unknownIntent_actionShouldIssueWarning() {
    // Arrange.
    LogFlags.logAccessibilityTree = false
    val receiver = FlagsBroadcastReceiver()
    val intent = Intent("SOME_WEIRD_ACTION")

    // Act.
    receiver.onReceive(context = null, intent = intent)

    // Assert.
    assertThat(LogFlags.logAccessibilityTree).isFalse()
  }

  @Test
  fun onReceive_intentWithDisableAction_shouldDisableTreeLogging() {
    // Arrange.
    LogFlags.logAccessibilityTree = true
    val receiver = FlagsBroadcastReceiver()
    val intent = Intent("accessibility_forwarder.intent.action.DISABLE_ACCESSIBILITY_TREE_LOGS")

    // Act.
    receiver.onReceive(context = null, intent = intent)

    // Assert.
    assertThat(LogFlags.logAccessibilityTree).isFalse()
  }

  @Test
  fun onReceive_intentWithEnableAction_shouldEnableTreeLogging() {
    // Arrange.
    LogFlags.logAccessibilityTree = false
    val receiver = FlagsBroadcastReceiver()
    val intent = Intent("accessibility_forwarder.intent.action.ENABLE_ACCESSIBILITY_TREE_LOGS")

    // Act.
    receiver.onReceive(context = null, intent = intent)

    // Assert.
    assertThat(LogFlags.logAccessibilityTree).isTrue()
  }

  @Test
  fun onReceive_intentWithSetGrpcActionNoArgs_shouldDefaultToEmuIpAndPortZero() {
    // Arrange.
    LogFlags.grpcHost = "some_host"
    LogFlags.grpcPort = 9999
    val receiver = FlagsBroadcastReceiver()
    val intent = Intent("accessibility_forwarder.intent.action.SET_GRPC")

    // Act.
    receiver.onReceive(context = null, intent = intent)

    // Assert.
    assertThat(LogFlags.grpcHost).isEqualTo("10.0.2.2")
    assertThat(LogFlags.grpcPort).isEqualTo(0)
  }

  @Test
  fun onReceive_intentWithSetGrpcActionWithHostNoPort_shouldDefaultPortToZero() {
    // Arrange.
    LogFlags.grpcHost = "some_host"
    LogFlags.grpcPort = 9999
    val receiver = FlagsBroadcastReceiver()
    val intent =
      Intent("accessibility_forwarder.intent.action.SET_GRPC").apply {
        putExtra("host", "awesome.server.ca")
      }

    // Act.
    receiver.onReceive(context = null, intent = intent)

    // Assert.
    assertThat(LogFlags.grpcHost).isEqualTo("awesome.server.ca")
    assertThat(LogFlags.grpcPort).isEqualTo(0)
  }

  @Test
  fun onReceive_intentWithSetGrpcActionWithPortNoHost_shouldDefaultHostToEmuIp() {
    // Arrange.
    LogFlags.grpcHost = "some_host"
    LogFlags.grpcPort = 9999
    val receiver = FlagsBroadcastReceiver()
    val intent =
      Intent("accessibility_forwarder.intent.action.SET_GRPC").apply { putExtra("port", 54321) }

    // Act.
    receiver.onReceive(context = null, intent = intent)

    // Assert.
    assertThat(LogFlags.grpcHost).isEqualTo("10.0.2.2")
    assertThat(LogFlags.grpcPort).isEqualTo(54321)
  }

  @Test
  fun onReceive_intentWithSetGrpcActionWithHostAndPort_shouldSetBoth() {
    // Arrange.
    LogFlags.grpcHost = "some_host"
    LogFlags.grpcPort = 9999
    val receiver = FlagsBroadcastReceiver()
    val intent =
      Intent("accessibility_forwarder.intent.action.SET_GRPC").apply {
        putExtra("host", "grpc.ca")
        putExtra("port", 54321)
      }

    // Act.
    receiver.onReceive(context = null, intent = intent)

    // Assert.
    assertThat(LogFlags.grpcHost).isEqualTo("grpc.ca")
    assertThat(LogFlags.grpcPort).isEqualTo(54321)
  }
}


================================================
FILE: android_env/apps/java/com/google/androidenv/accessibilityforwarder/LogFlags.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.accessibilityforwarder

/**
 * Controls global settings in AccessibilityForwarder.
 *
 * Please note that this class is not thread safe.
 */
object LogFlags {
  // Whether to log the accessibility tree.
  var logAccessibilityTree: Boolean = false
  // How frequent to emit a11y trees (in milliseconds).
  var a11yTreePeriodMs: Long = 100

  // The gRPC server to connect to. (Only available if grpcPort>0).
  var grpcHost: String = ""
  // If >0 this represents the gRPC port number to connect to.
  var grpcPort: Int = 0
}


================================================
FILE: android_env/apps/java/com/google/androidenv/accessibilityforwarder/ParentChildNodePair.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.accessibilityforwarder

import android.view.accessibility.AccessibilityNodeInfo
import com.google.auto.value.AutoValue

/** Parent and child [AccessibilityNodeInfo] relationship. */
@AutoValue
internal abstract class ParentChildNodePair {
  abstract fun parent(): AccessibilityNodeInfo?

  abstract fun child(): AccessibilityNodeInfo

  /** [ParentChildNodePair] builder. */
  @AutoValue.Builder
  abstract class Builder {
    abstract fun parent(parent: AccessibilityNodeInfo?): Builder

    abstract fun child(child: AccessibilityNodeInfo): Builder

    abstract fun build(): ParentChildNodePair
  }

  companion object {
    @JvmStatic fun builder(): Builder = AutoValue_ParentChildNodePair.Builder()
  }
}


================================================
FILE: android_env/apps/java/com/google/androidenv/accessibilityforwarder/UniqueIdsGenerator.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.accessibilityforwarder

import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import java.util.function.Function

/** Thread-safe helper class for assigning a unique ID to an object. */
internal class UniqueIdsGenerator<A : Any> {
  private val nextId = AtomicInteger(0)
  private val uniqueIdsByNode = ConcurrentHashMap<A, Int>()

  fun getUniqueId(a: A): Int {
    return uniqueIdsByNode.computeIfAbsent(a, Function { _: A -> nextId.getAndIncrement() })!!
  }
}


================================================
FILE: android_env/apps/java/com/google/androidenv/accessibilityforwarder/res/xml/accessibility_forwarder_service.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2026 DeepMind Technologies Limited.

 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.-->



<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagReportViewIds"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:canRetrieveWindowContent="true"/>


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2026 DeepMind Technologies Limited.

 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.-->


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.google.androidenv.catch">
    <uses-sdk android:minSdkVersion="26"
      android:targetSdkVersion="35"/>
    <application
        android:allowBackup="false"
        android:label="@string/app_name"
        android:supportsRtl="false"
        android:taskAffinity=""
        tools:ignore="AllowBackup">
        <activity
            android:name=".MainActivity"
            android:configChanges="orientation|keyboardHidden|screenSize"
            android:exported="true"
            android:hardwareAccelerated="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/BUILD.bazel
================================================
# Copyright 2026 DeepMind Technologies Limited.
#
# 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.

# Classic RL task implemented as an Android app.
load("@rules_android//rules:rules.bzl", "android_binary")
load("@rules_kotlin//kotlin:android.bzl", "kt_android_library")

package(
    default_visibility = [":catch_packages"],
)

package_group(
    name = "catch_packages",
    packages = [
        "//java/com/google/androidenv/catch/...",
        "//javatests/com/google/androidenv/catch/...",
    ],
)

licenses(["notice"])

android_binary(
    name = "app",
    manifest = "AndroidManifest.xml",
    multidex = "native",
    deps = [":MainActivity"],
)

kt_android_library(
    name = "GameLogic",
    srcs = ["GameLogic.kt"],
    deps = [
        "//java/com/google/androidenv/catch/sprite:Background",
        "//java/com/google/androidenv/catch/sprite:Ball",
        "//java/com/google/androidenv/catch/sprite:LineSegment",
        "//java/com/google/androidenv/catch/sprite:Paddle",
    ],
)

kt_android_library(
    name = "GameLogicThread",
    srcs = ["GameLogicThread.kt"],
    deps = [
        ":GameLogic",
    ],
)

kt_android_library(
    name = "MainActivity",
    srcs = ["MainActivity.kt"],
    manifest = "AndroidManifest.xml",
    resource_files = glob(["res/**"]),
    deps = [
        ":GameLogic",
        ":GameLogicThread",
        ":RenderThread",
        "//java/com/google/androidenv/catch/sprite:Background",
        "//java/com/google/androidenv/catch/sprite:Ball",
        "//java/com/google/androidenv/catch/sprite:Paddle",
    ],
)

kt_android_library(
    name = "RenderThread",
    srcs = ["RenderThread.kt"],
    deps = [
        ":GameLogic",
    ],
)


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/GameLogic.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch

import android.graphics.Canvas
import android.view.MotionEvent
import com.google.androidenv.catch.sprite.Background
import com.google.androidenv.catch.sprite.Ball
import com.google.androidenv.catch.sprite.LineSegment
import com.google.androidenv.catch.sprite.Paddle
import java.time.Duration
import java.time.Instant
import kotlin.random.Random

/** The class that contains the game logic. */
open class GameLogic(
  // Expected number of frames per second.
  fps: Int = 60,
  // Pseudo random number generator.
  private val rand: Random = Random.Default,
  // Width and height of the game in pixels.
  private val width: Int,
  private val height: Int,
  // UI objects in the game.
  private var background: Background = Background(),
  private var ball: Ball = Ball(maxX = width, maxY = height, rand = rand),
  private var paddle: Paddle = Paddle(maxX = width, y = height),
) {

  private val sleepTime: Duration = Duration.ofMillis((1000.0 / fps).toLong())

  /** Reinitializes the state of the game. */
  // Need to make this open to allow for testing.
  open fun reset() {
    this.ball.reset()
  }

  /** Runs one "throw" of a [ball] that needs to be caught by the [paddle]. */
  // Need to make this open to allow for testing.
  open fun run(): Boolean {
    var lastTimestamp = Instant.now()
    do {
      Thread.sleep(sleepTime.toMillis())
      val now = Instant.now()
      val interval = Duration.between(lastTimestamp, now)
      lastTimestamp = now
      ball.update(interval)
    } while (!ball.isOutOfBounds())

    return ball.intersects(LineSegment(paddle.topLeft(), paddle.topRight()))
  }

  /** Processes a user event (e.g. a touchscreen event) and updates the [paddle] accordingly. */
  fun handleTouch(event: MotionEvent) {
    paddle.x = event.x.toInt()
  }

  /** Renders the game on [c]. */
  open fun render(c: Canvas) {
    background.draw(c)
    ball.draw(c)
    paddle.draw(c)
  }
}


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/GameLogicThread.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch

import android.util.Log

/** A thread that continuously runs the game logic, resetting after each internal [run()]. */
class GameLogicThread(private val game: GameLogic, private val loggingTag: String) : Thread() {

  /** Whether this thread should continuously run. */
  private var shouldRun: Boolean = true
  /** A counter of game runs. */
  private var counter: Int = 0

  /**
   * Lets the current [run()] iteration complete then break exit this [Thread].
   *
   * Notice that [shouldRun] cannot have a private getter with a public setter (please see
   * https://youtrack.jetbrains.com/issue/KT-3110 for details), hence this public function. Also
   * notice that we cannot call this function [stop()] since it would shadow [Thread.stop()].
   */
  public fun finish() {
    shouldRun = false
  }

  /** Continuously runs the [game] until [finish()] is called. */
  public override fun run() {
    while (shouldRun) {
      game.reset()
      Log.i(loggingTag, "${counter++} - ${game.run()}")
    }
  }
}


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/MainActivity.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch

import android.app.Activity
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.view.Window
import com.google.androidenv.catch.sprite.Background
import com.google.androidenv.catch.sprite.Ball
import com.google.androidenv.catch.sprite.Paddle

/** The activity that allows users to play the RL game of Catch. */
class MainActivity() : Activity(), SurfaceHolder.Callback {

  private var surfaceView: SurfaceView? = null
  private var renderThread: RenderThread? = null
  private var gameLogicThread: GameLogicThread? = null

  private val fps: Int = 60
  private var gameCounter: Int = 0
  private var width: Int = -1
  private var height: Int = -1

  private var extras: Bundle? = null

  // [Activity] overrides.

  /** Initializes the Android [View] and sets up callbacks. */
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.i(TAG, "MainActivity::onCreate()")
    requestWindowFeature(Window.FEATURE_NO_TITLE)
    setContentView(R.layout.main)
    val surface: SurfaceView? = findViewById(R.id.surfaceView)
    if (surface == null) throw Exception("Could not create SurfaceView. Aborting...")

    surface.visibility = View.VISIBLE
    surface.holder.addCallback(this)
    surfaceView = surface
    extras = intent?.extras
  }

  override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    Log.i(TAG, "MainActivity::onNewIntent()")
    extras = intent?.extras
    startGame()
  }

  // [SurfaceHolder.Callback] overrides.

  override fun surfaceCreated(holder: SurfaceHolder) {
    Log.i(TAG, "MainActivity::surfaceCreated()")
    renderThread = RenderThread(surfaceHolder = holder, fps = fps).also { it.start() }
  }

  override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
    Log.i(TAG, "MainActivity::surfaceChanged()")
    this.width = width
    this.height = height
    startGame()
  }

  override fun surfaceDestroyed(holder: SurfaceHolder) {
    Log.i(TAG, "MainActivity::surfaceDestroyed()")
    renderThread?.finish()
    renderThread?.join()
    gameLogicThread?.finish()
    gameLogicThread?.join()
  }

  private fun startGame() {
    Log.i(TAG, "MainActivity::startGame()")
    if (width <= 0 || height <= 0) {
      Log.e(TAG, "MainActivity::startGame() - Width or height not initialized yet.")
      return
    }
    val backgroundColor = Color.parseColor(extras?.getString("backgroundColor") ?: "BLACK")
    val ballColor = Color.parseColor(extras?.getString("ballColor") ?: "WHITE")
    val ballRadius = extras?.getFloat("ballRadius", 10.0f) ?: 10.0f
    val ballSpeed = extras?.getFloat("ballSpeed", 0.2f) ?: 0.2f
    val paddleColor = Color.parseColor(extras?.getString("paddleColor") ?: "WHITE")
    val paddleWidth = extras?.getInt("paddleWidth", 80) ?: 80
    val paddleHeight = extras?.getInt("paddleHeight", 10) ?: 10
    Log.i(TAG, "MainActivity::startGame() - extras bundle: $extras")
    val game =
      GameLogic(
        width = width,
        height = height,
        fps = fps,
        background = Background(color = backgroundColor),
        ball =
          Ball(
            maxX = width,
            maxY = height,
            color = ballColor,
            radius = ballRadius,
            speed = ballSpeed,
          ),
        paddle =
          Paddle(
            color = paddleColor,
            width = paddleWidth,
            height = paddleHeight,
            maxX = width,
            y = (height - paddleHeight / 2),
          ),
      )

    // Stop the previous game logic thread if it's running.
    gameLogicThread?.finish()
    gameLogicThread?.join()

    // Create and start the new GameLogicThread, passing the game instance.
    gameLogicThread = GameLogicThread(game, TAG).also { it.start() }

    // Pass the same game instance to the render thread.
    renderThread?.game = game

    surfaceView?.setOnTouchListener(
      // Suppress warning for ClickableViewAccessibility since click handling
      // is not within an OnTouchListener.
      @SuppressWarnings("ClickableViewAccessibility")
      View.OnTouchListener { _, motionEvent ->
        game.handleTouch(motionEvent)
        true
      }
    )
  }

  companion object {
    private const val TAG = "AndroidRLTask"
  }
}


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/RenderThread.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch

import android.graphics.Canvas
import android.view.SurfaceHolder
import java.time.Duration

/** A thread that continuously renders the game logic onto a surface. */
class RenderThread(private val surfaceHolder: SurfaceHolder, private val fps: Int = 60) : Thread() {

  /** Whether this thread should continuously run. */
  private var shouldRun: Boolean = true
  /** How long to sleep at each [run()] iteration. */
  private val sleepTime: Duration = Duration.ofMillis((1000.0 / fps).toLong())
  /** The class responsible for issuing rendering commands to the canvas. */
  var game: GameLogic? = null

  /**
   * Runs the current game logic [run()] to completion.
   *
   * Notice that [shouldRun] cannot have a private getter with a public setter (please see
   * https://youtrack.jetbrains.com/issue/KT-3110 for details), hence this public function. Also
   * notice that we cannot call this function [stop()] since it would shadow [Thread.stop()].
   */
  public fun finish() {
    shouldRun = false
  }

  /** Continuously renders the [game] onto [surfaceHolder]. */
  public override fun run() {
    while (shouldRun) {
      if (surfaceHolder.surface?.isValid() ?: false) {
        val c: Canvas = surfaceHolder.lockCanvas()
        game?.render(c)
        surfaceHolder.unlockCanvasAndPost(c)
      }
      Thread.sleep(sleepTime.toMillis())
    }
  }
}


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/res/layout/main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2026 DeepMind Technologies Limited.

 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.-->


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <SurfaceView
        android:id="@+id/surfaceView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone" />
</RelativeLayout>


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/res/values/strings.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2026 DeepMind Technologies Limited.

 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.-->


<resources>
  <string name="app_name">Catch</string>
</resources>


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/sprite/BUILD.bazel
================================================
# Copyright 2026 DeepMind Technologies Limited.
#
# 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.

# Sprites for the app.

load("@rules_kotlin//kotlin:android.bzl", "kt_android_library")

package(
    default_visibility = ["//java/com/google/androidenv/catch:catch_packages"],
)

licenses(["notice"])

kt_android_library(
    name = "Background",
    srcs = ["Background.kt"],
    deps = [":Sprite"],
)

kt_android_library(
    name = "Ball",
    srcs = ["Ball.kt"],
    deps = [
        ":LineSegment",
        ":Point",
        ":Sprite",
    ],
)

kt_android_library(
    name = "LineSegment",
    srcs = ["LineSegment.kt"],
    deps = [":Point"],
)

kt_android_library(
    name = "Paddle",
    srcs = ["Paddle.kt"],
    deps = [
        ":Point",
        ":Sprite",
    ],
)

kt_android_library(
    name = "Point",
    srcs = ["Point.kt"],
)

kt_android_library(
    name = "Sprite",
    srcs = ["Sprite.kt"],
)


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/sprite/Background.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch.sprite

import android.graphics.Canvas
import android.graphics.Color

/** Represents the static background behind all objects. */
open class Background(private val color: Int = Color.BLACK) : Sprite() {
  /** Paints the canvas with the color given in the constructor. */
  override fun draw(c: Canvas) {
    c.drawColor(color)
  }
}


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/sprite/Ball.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch.sprite

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import java.time.Duration
import kotlin.math.ceil
import kotlin.math.sqrt
import kotlin.random.Random

/** Represents a ball that travels down in space with constant speed. */
open class Ball(
  private val maxX: Int,
  private val maxY: Int,
  private val color: Int = Color.WHITE,
  private val radius: Float = 10.0f,
  // `speed`'s unit is in pixels/ms.
  private val speed: Float = 1.0f,
  private val rand: Random = Random.Default,
) : Sprite() {

  // `x` and `y` represent the position of the center of this ball.
  //
  // Valid range [0, maxX]. 0==left, maxX==right.
  private var x: Int = rand.nextInt(maxX)
  // Valid range [0, maxY]. 0==top, maxY==bottom.
  private var y: Int = ceil(radius).toInt()

  private val paint: Paint =
    Paint(Paint.ANTI_ALIAS_FLAG).apply {
      style = Paint.Style.FILL
      color = (this@Ball).color
    }

  /** Returns `true` if this ball intersects the given line [segment]. */
  fun intersects(segment: LineSegment): Boolean {

    /** A vector with two components. */
    data class Vector2D(val u: Int, val v: Int) {
      /** Returns the dot product between two 2D vectors. */
      fun dot(other: Vector2D): Int = u * other.u + v * other.v
    }

    /** Returns the vector representing [p] minus [q]. */
    fun pointDiff(p: Point, q: Point): Vector2D = Vector2D(p.x - q.x, p.y - q.y)

    val direction = pointDiff(segment.p1, segment.p0) // p0 -> p1.
    val centerToP = pointDiff(segment.p0, Point(x, y)) // Ball center -> p0.

    // The `(centerToP + m * direction)` function models all the points in the line segment where
    // the independent variable `m` is a real number in [0,1]. Putting this function into the
    // formula for the circle (x ^ 2 + y ^ 2 = radius ^ 2) gives a quadratic equation
    // (am^2 + bm + c = 0) where:
    // [a] = direction · direction
    // [b] = 2 centerToP · direction
    // [c] = centerToP · centerToP - radius ^ 2
    val a = direction.dot(direction)
    val b = 2 * centerToP.dot(direction)
    val c = centerToP.dot(centerToP) - radius * radius

    val delta = b * b - 4 * a * c
    if (delta < 0)
      return false // No real roots means the (infinite) line does not intersect the ball.

    val d = sqrt(delta)
    val m1 = (-b - d) / (2 * a)
    val m2 = (-b + d) / (2 * a)

    // If a root is in [0,1], the line segment intersects the circumference.
    // If [m1] < 0 and [m2] > 1, the line segment is "within" the circle meaning the circle
    // intersects the infinite line, but not the line segment. In this case, we consider that it
    // touched the ball.
    return (m1 >= 0 && m1 <= 1) || (m2 >= 0 && m2 <= 1) || (m1 < 0 && m2 > 1)
  }

  /** Places the ball at the top of the screen at a random x-coordinate. */
  fun reset() {
    x = rand.nextInt(maxX)
    y = ceil(radius).toInt()
  }

  /** Moves the ball down by [timeDeltaMs]. */
  open fun update(timeDelta: Duration) {
    y += (speed * timeDelta.toMillis()).toInt()
  }

  /** Returns whether the ball is over [maxY]. */
  fun isOutOfBounds(): Boolean = y + radius > maxY || y - radius < 0

  /** Draws this ball in `c`. */
  override fun draw(c: Canvas) {
    c.drawCircle(x.toFloat(), y.toFloat(), radius, paint)
  }
}


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/sprite/LineSegment.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch.sprite

/** Represents a finite line segment in 2D connected by two points [p0] and [p1]. */
data class LineSegment(val p0: Point, val p1: Point)


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/sprite/Paddle.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch.sprite

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import kotlin.ranges.coerceIn

/** Represents a paddle to hit/catch a falling ball. */
open class Paddle(
  private val color: Int = Color.WHITE,
  // Width and height in pixels.
  private val width: Int = 80,
  private val height: Int = 10,
  // maxX is the maximum X value for the center of the paddle.
  private val maxX: Int = 100,
  // The vertical position of the center of this paddle in pixels.
  val y: Int = 100,
) : Sprite() {

  // Memoize a few things to make [draw()] a bit faster.
  private val halfH = height / 2
  private val halfW = width / 2
  private val paint =
    Paint(Paint.ANTI_ALIAS_FLAG).apply {
      style = Paint.Style.FILL
      color = (this@Paddle).color
    }

  // The horizontal center of the paddle.
  var x: Int = maxX / 2 // Start in the middle.
    set(value) {
      field = value.coerceIn(0, maxX)
    }

  /** Returns the (x,y) coordinates of the top-left corner. */
  fun topLeft(): Point = Point(x - halfW, y - halfH)

  /** Returns the (x,y) coordinates of the top-right corner. */
  fun topRight(): Point = Point(x + halfW, y - halfH)

  fun move(deltaX: Int) {
    x += deltaX
  }

  override fun draw(c: Canvas) {
    val rect =
      Rect().apply {
        bottom = y + halfH
        top = y - halfH
        left = x - halfW
        right = x + halfW
      }
    c.drawRect(rect, paint)
  }
}


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/sprite/Point.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch.sprite

/** Represents a cartesian point in 2D. */
data class Point(val x: Int, val y: Int)


================================================
FILE: android_env/apps/java/com/google/androidenv/catch/sprite/Sprite.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch.sprite

import android.graphics.Canvas

/** Represents something that can be drawn on the screen. */
open class Sprite {

  /** Draws the Sprite in the given canvas. */
  open fun draw(c: Canvas) {}
}


================================================
FILE: android_env/apps/javatests/com/google/androidenv/catch/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2026 DeepMind Technologies Limited.

 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.-->


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.google.androidenv.catch">
    <uses-sdk android:minSdkVersion="26"
      android:targetSdkVersion="35"/>
    <application
        android:allowBackup="false"
        android:label="@string/app_name"
        android:supportsRtl="false"
        android:taskAffinity=""
        tools:ignore="AllowBackup">
        <activity
            android:name=".MainActivity"
            android:configChanges="orientation|keyboardHidden|screenSize"
            android:exported="true"
            android:hardwareAccelerated="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>


================================================
FILE: android_env/apps/javatests/com/google/androidenv/catch/BUILD.bazel
================================================
# Copyright 2026 DeepMind Technologies Limited.
#
# 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.

# Tests for the Android version of the RL Catch game.
load("@rules_kotlin//kotlin:android.bzl", "kt_android_local_test")
load("@rules_kotlin//kotlin:core.bzl", "kt_kotlinc_options")

kt_kotlinc_options(
    name = "kt_kotlinc_options",
    jvm_target = "11",  # Need to override default 1.8.
    x_no_param_assertions = True,
)

kt_android_local_test(
    name = "GameLogicTest",
    srcs = ["GameLogicTest.kt"],
    kotlinc_opts = ":kt_kotlinc_options",
    deps = [
        "//java/com/google/androidenv/catch:GameLogic",
        "//java/com/google/androidenv/catch/sprite:Background",
        "//java/com/google/androidenv/catch/sprite:Ball",
        "//java/com/google/androidenv/catch/sprite:Paddle",
        "@maven//:androidx_test_ext_junit",
        "@maven//:androidx_test_runner",
        "@maven//:com_google_truth_truth",
        "@maven//:org_mockito_kotlin_mockito_kotlin",
        "@maven//:org_robolectric_robolectric",
        "@robolectric//bazel:android-all",
    ],
)

kt_android_local_test(
    name = "GameLogicThreadTest",
    srcs = ["GameLogicThreadTest.kt"],
    kotlinc_opts = ":kt_kotlinc_options",
    deps = [
        "//java/com/google/androidenv/catch:GameLogic",
        "//java/com/google/androidenv/catch:GameLogicThread",
        "@maven//:androidx_test_ext_junit",
        "@maven//:com_google_truth_truth",
        "@maven//:org_mockito_kotlin_mockito_kotlin",
        "@maven//:org_robolectric_robolectric",
        "@robolectric//bazel:android-all",
    ],
)

kt_android_local_test(
    name = "MainActivityTest",
    srcs = [
        "MainActivityTest.kt",
    ],
    kotlinc_opts = ":kt_kotlinc_options",
    manifest = "AndroidManifest.xml",
    deps = [
        "//java/com/google/androidenv/catch:MainActivity",
        "@maven//:androidx_test_ext_junit",
        "@maven//:junit_junit",
        "@maven//:org_robolectric_robolectric",
        "@robolectric//bazel:android-all",
    ],
)

kt_android_local_test(
    name = "RenderThreadTest",
    srcs = ["RenderThreadTest.kt"],
    kotlinc_opts = ":kt_kotlinc_options",
    deps = [
        "//java/com/google/androidenv/catch:GameLogic",
        "//java/com/google/androidenv/catch:RenderThread",
        "@maven//:androidx_test_ext_junit",
        "@maven//:org_mockito_kotlin_mockito_kotlin",
        "@maven//:org_mockito_mockito_core",
        "@maven//:org_robolectric_robolectric",
        "@robolectric//bazel:android-all",
    ],
)


================================================
FILE: android_env/apps/javatests/com/google/androidenv/catch/GameLogicTest.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch

import android.graphics.Canvas
import androidx.test.core.view.MotionEventBuilder
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.androidenv.catch.sprite.Background
import com.google.androidenv.catch.sprite.Ball
import com.google.androidenv.catch.sprite.Paddle
import com.google.common.truth.Truth.assertThat
import java.time.Duration
import java.time.Instant
import kotlin.random.Random
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.atLeast
import org.mockito.kotlin.atMost
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.verify

@RunWith(AndroidJUnit4::class)
class GameLogicTest {

  @Test
  fun run_ballIsMissed() {
    // Arrange.
    val width = 123
    val height = 33
    val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 37 }
    val game =
      GameLogic(
        rand = mockRandom,
        width = width,
        height = height,
        ball = Ball(maxX = width, maxY = height, radius = 5.0f, rand = mockRandom),
        paddle = Paddle(maxX = width, y = height, width = 3, height = 2),
      )
    game.reset()
    game.handleTouch(
      MotionEventBuilder.newBuilder().setPointer(/* x= */ 12.0f, /* y= */ 31.0f).build()
    )

    // Act.
    val outcome = game.run() // Ball falls at x==37, ev.x==12 so ball is missed.

    // Assert.
    assertThat(outcome).isEqualTo(false)
  }

  @Test
  fun run_ballIsCaught() {
    // Arrange.
    val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 53 }
    val game = GameLogic(rand = mockRandom, width = 321, height = 47)
    game.reset()
    game.handleTouch(
      MotionEventBuilder.newBuilder().setPointer(/* x= */ 53.0f, /* y= */ 43.0f).build()
    )

    // Act.
    val outcome = game.run() // Ball falls at x==53, ev.x==53 so ball is caught.

    // Assert.
    assertThat(outcome).isEqualTo(true)
  }

  @Test
  fun run_resetAllowsMultipleGamesToBePlayedWithASingleObjectAndDoesNotHang() {
    // Arrange.
    val mockRandom: Random = mock()
    val game = GameLogic(width = 101, height = 59, rand = mockRandom)

    // Act.
    repeat(17) {
      game.reset()
      val unused = game.run() // Ignore the outcome since we only care about run() terminating.
    }

    // Assert.
    // [rand.nextInt()] should be called once at construction and then 17 times for [reset()].
    verify(mockRandom, times(18)).nextInt(any())
  }

  @Test
  fun run_inASeparateThread() {
    // Arrange.
    val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 23 }
    val game = GameLogic(rand = mockRandom, width = 321, height = 89)
    game.reset()
    game.handleTouch(
      MotionEventBuilder.newBuilder().setPointer(/* x= */ 23.0f, /* y= */ 29.0f).build()
    )
    var outcome: Boolean = false

    class MyThread(val g: GameLogic, var outcome: Boolean) : Thread() {
      public override fun run() {
        outcome = g.run()
      }
    }
    val someThread = MyThread(game, outcome)

    // Act.
    someThread.start() // Ball falls at x==23, ev.x==23 so ball is caught.
    someThread.join()

    // Assert.
    assertThat(outcome).isEqualTo(true)
  }

  @Test
  fun run_fpsLeadstoApproximatelyNumberOfElapsedTimeAndUpdateCalls() {
    // Arrange.
    val width = 123
    val height = 300
    val ball = spy(Ball(maxX = width, maxY = height, speed = 2.0f, radius = 1.0f))
    val game = GameLogic(fps = 100, width = width, height = height, ball = ball)
    game.reset()

    // Act.
    val start = Instant.now()
    val unused = game.run()
    val end = Instant.now()

    // Assert.
    val elapsed = Duration.between(start, end)
    // The ball should take around `height / speed = 150` milliseconds to reach the bottom. Due to
    // timing non-determinism, we accept values between 100 and 200.
    assertThat(elapsed.toMillis()).isAtLeast(100L)
    assertThat(elapsed.toMillis()).isAtMost(200L)
    // At fps==100, we expect [update()] to be called every `1000 / 100 = 10` milliseconds. We
    // expect [elapsed] to be around 150ms (checked above) which should be around `150 / 10 = 15`
    // calls, so to account for timing non-determinism we accept between 5 and 25 calls.
    verify(ball, atLeast(5)).update(any())
    verify(ball, atMost(25)).update(any())
  }

  @Test
  fun render_drawCanBeCalledMultipleTimesWithinASingleRun() {
    // Arrange.
    val width = 321
    val height = 89
    val mockCanvas: Canvas = mock()
    val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 23 }
    val background = spy(Background())
    val paddle = spy(Paddle())
    val ball = spy(Ball(maxX = width, maxY = height))
    val game =
      GameLogic(
        rand = mockRandom,
        width = width,
        height = height,
        background = background,
        ball = ball,
        paddle = paddle,
      )
    game.reset()
    game.handleTouch(
      MotionEventBuilder.newBuilder().setPointer(/* x= */ 23.0f, /* y= */ 29.0f).build()
    )

    class MyThread(val g: GameLogic) : Thread() {
      public override fun run() {
        val unused = g.run()
      }
    }
    val someThread = MyThread(game)

    // Act.
    someThread.start()
    repeat(11) { game.render(mockCanvas) }
    someThread.join()

    // Assert.
    verify(background, times(11)).draw(mockCanvas)
    verify(ball, times(11)).draw(mockCanvas)
    verify(paddle, times(11)).draw(mockCanvas)
  }
}


================================================
FILE: android_env/apps/javatests/com/google/androidenv/catch/GameLogicThreadTest.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch

import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.robolectric.junit.rules.ExpectedLogMessagesRule

@RunWith(AndroidJUnit4::class)
class GameLogicThreadTest {

  // Rule to assert log messages, taken as a reference from MainActivityTest.kt
  @get:Rule val expectedLogMessagesRule = ExpectedLogMessagesRule()

  private val mockGame: GameLogic = mock()
  private val testTag = "TestAndroidRLTask"

  @Test
  fun run_iteratesGameAndLogs() {
    // Arrange
    val gameLogicThread = GameLogicThread(mockGame, testTag)

    // Act
    gameLogicThread.start()
    Thread.sleep(100) // Allow time for the thread to execute at least once.
    gameLogicThread.finish()
    gameLogicThread.join() // Wait for the thread to terminate.

    // Assert
    // Verify that the game's core methods were called at least once.
    verify(mockGame, atLeastOnce()).reset()
    verify(mockGame, atLeastOnce()).run()
    // Expect the log message from the run() loop.
    // The mock 'game.run()' returns false by default.
    expectedLogMessagesRule.expectLogMessage(Log.INFO, testTag, "0 - false")
  }

  @Test
  fun finish_stopsTheThread() {
    // Arrange
    val gameLogicThread = GameLogicThread(mockGame, testTag)

    // Act
    gameLogicThread.start()
    // Let it run for a moment before stopping it.
    Thread.sleep(50)
    gameLogicThread.finish()
    gameLogicThread.join()

    // Assert
    assertThat(gameLogicThread.isAlive).isFalse()
  }
}


================================================
FILE: android_env/apps/javatests/com/google/androidenv/catch/MainActivityTest.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch

import android.content.Intent
import android.util.Log
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import java.lang.reflect.Method
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.junit.rules.ExpectedLogMessagesRule

@RunWith(AndroidJUnit4::class)
class MainActivityTest {
  @get:Rule(order = 0) val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)
  @get:Rule(order = 1) val expectedLogMessagesRule = ExpectedLogMessagesRule()

  @Before
  fun setUp() {
    expectedLogMessagesRule.expectLogMessage(Log.INFO, TAG, "MainActivity::onCreate()")
  }

  @Test
  fun surfaceChanged_logsStartsGame() {
    activityScenarioRule.scenario.onActivity { activity ->
      // Arrange.
      val surfaceView = activity.findViewById<android.view.SurfaceView>(R.id.surfaceView)
      val surfaceHolder = surfaceView.holder

      // Act - Trigger the surfaceChanged callback with positive width and height.
      activity.surfaceChanged(surfaceHolder, 0, 100, 200)

      // Assert.
      expectedLogMessagesRule.expectLogMessage(Log.INFO, TAG, "MainActivity::surfaceChanged()")
      expectedLogMessagesRule.expectLogMessage(Log.INFO, TAG, "MainActivity::startGame()")
    }
  }

  @Test
  fun onNewIntent_logsStartsGame_errorsOnUninitializedWidthOrHeight() {
    // Arrange.
    val newIntent = Intent()
    // Find the onNewIntent method using reflection
    val onNewIntentMethod: Method =
      MainActivity::class.java.getDeclaredMethod("onNewIntent", Intent::class.java)
    // Enable access to protected method
    onNewIntentMethod.isAccessible = true

    activityScenarioRule.scenario.onActivity { activity ->
      // Act - Invoke the onNewIntent method using reflection.
      onNewIntentMethod.invoke(activity, newIntent)

      // Assert.
      expectedLogMessagesRule.expectLogMessage(Log.INFO, TAG, "MainActivity::onNewIntent()")
      expectedLogMessagesRule.expectLogMessage(Log.INFO, TAG, "MainActivity::startGame()")
      // In this test case where we don't call surfaceChanged(), default width and height
      // are -1 and should trigger this error to prevent Ball from initializing
      // with invalid negative values, since nextInt() expects a positive number.
      expectedLogMessagesRule.expectLogMessage(
        Log.ERROR,
        TAG,
        "MainActivity::startGame() - Width or height not initialized yet.",
      )
    }
  }

  companion object {
    private const val TAG = "AndroidRLTask"
  }
}


================================================
FILE: android_env/apps/javatests/com/google/androidenv/catch/RenderThreadTest.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch

import android.graphics.Canvas
import android.view.Surface
import android.view.SurfaceHolder
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verifyNoInteractions
import org.mockito.kotlin.any
import org.mockito.kotlin.atLeast
import org.mockito.kotlin.atMost
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify

@RunWith(AndroidJUnit4::class)
class RenderThreadTest {

  @Test
  fun run_finishBeforeStartResultsInNoRendering() {
    // Arrange.
    val surfaceHolder: SurfaceHolder = mock()
    val renderThread = RenderThread(surfaceHolder = surfaceHolder, fps = 1000)
    val game: GameLogic = mock()
    renderThread.game = game

    // Act.
    renderThread.finish()
    renderThread.start()

    // Assert.
    verifyNoInteractions(game)
    verifyNoInteractions(surfaceHolder)
  }

  @Test
  fun run_startResultsInSomeRendering() {
    // Arrange.
    val canvas: Canvas = mock()
    val surface: Surface = mock() { on { isValid() } doReturn true }
    val surfaceHolder: SurfaceHolder =
      mock() {
        on { getSurface() } doReturn surface
        on { lockCanvas() } doReturn canvas
      }
    val renderThread = RenderThread(surfaceHolder = surfaceHolder, fps = 1000)
    val game: GameLogic = mock()
    renderThread.game = game

    // Act.
    renderThread.start()
    Thread.sleep(/* millis= */ 500) // Sleep for at least one loop iteration.
    renderThread.finish()

    // Assert.
    verify(surfaceHolder, atLeast(1)).surface
    verify(surfaceHolder, atLeast(1)).lockCanvas()
    verify(surfaceHolder, atLeast(1)).unlockCanvasAndPost(any())
    verify(game, atLeast(1)).render(canvas)
  }

  @Test
  fun run_finishStopsRendering() {
    // Arrange.
    val canvas: Canvas = mock()
    val surface: Surface = mock() { on { isValid() } doReturn true }
    val surfaceHolder: SurfaceHolder =
      mock() {
        on { getSurface() } doReturn surface
        on { lockCanvas() } doReturn canvas
      }
    val renderThread = RenderThread(surfaceHolder = surfaceHolder, fps = 20)
    val game: GameLogic = mock()
    renderThread.game = game

    // Act.
    renderThread.start()
    Thread.sleep(/* millis= */ 500) // Sleep for around 10 iterations
    renderThread.finish()
    Thread.sleep(/* millis= */ 500) // Sleep some more to ensure nothing runs after.

    // Assert.
    verify(surfaceHolder, atLeast(1)).surface
    verify(surfaceHolder, atLeast(1)).lockCanvas()
    verify(surfaceHolder, atLeast(1)).unlockCanvasAndPost(any())
    // We expect [game.render()] to be executed for around 500 / (1000 / 20 = 50) = 10 times. To
    // allow for some timing non-determinism we allow it to execute up to 15 times, but not more
    // than that since [renderThread.finish()] should stop the thread from calling it.
    verify(game, atLeast(1)).render(canvas)
    verify(game, atMost(15)).render(canvas)
  }

  @Test
  fun run_expectedFramesPerSecond() {
    // Arrange.
    val canvas: Canvas = mock()
    val surface: Surface = mock() { on { isValid() } doReturn true }
    val surfaceHolder: SurfaceHolder =
      mock() {
        on { getSurface() } doReturn surface
        on { lockCanvas() } doReturn canvas
      }
    val renderThread = RenderThread(surfaceHolder = surfaceHolder, fps = 5)
    val game: GameLogic = mock()
    renderThread.game = game

    // Act.
    renderThread.start()
    Thread.sleep(/* millis= */ 2000) // Sleep for around 10 loop iterations.
    renderThread.finish()

    // Assert.
    verify(surfaceHolder, atLeast(1)).surface
    verify(surfaceHolder, atLeast(1)).lockCanvas()
    verify(surfaceHolder, atLeast(1)).unlockCanvasAndPost(any())
    // We expect [game.render()] to be called around 2000ms / 5fps = 10 times but to account for
    // timing non-determinism we allow ±4 iterations.
    verify(game, atLeast(6)).render(canvas)
    verify(game, atMost(14)).render(canvas)
  }
}


================================================
FILE: android_env/apps/javatests/com/google/androidenv/catch/sprite/BUILD.bazel
================================================
# Copyright 2026 DeepMind Technologies Limited.
#
# 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.

# Unit tests for Sprites in Catch.
load("@rules_kotlin//kotlin:android.bzl", "kt_android_local_test")
load("@rules_kotlin//kotlin:core.bzl", "kt_kotlinc_options")

kt_kotlinc_options(
    name = "kt_kotlinc_options",
    jvm_target = "11",  # Need to override default 1.8.
    x_no_param_assertions = True,
)

kt_android_local_test(
    name = "BackgroundTest",
    srcs = ["BackgroundTest.kt"],
    kotlinc_opts = ":kt_kotlinc_options",
    deps = [
        "//java/com/google/androidenv/catch/sprite:Background",
        "@maven//:com_google_guava_guava",
        "@maven//:com_google_testparameterinjector_test_parameter_injector",
        "@maven//:org_mockito_kotlin_mockito_kotlin",
        "@maven//:org_yaml_snakeyaml",
    ],
)

kt_android_local_test(
    name = "BallTest",
    srcs = ["BallTest.kt"],
    kotlinc_opts = ":kt_kotlinc_options",
    tags = ["robolectric"],
    deps = [
        "//java/com/google/androidenv/catch/sprite:Ball",
        "//java/com/google/androidenv/catch/sprite:LineSegment",
        "//java/com/google/androidenv/catch/sprite:Point",
        "@maven//:androidx_test_ext_junit",
        "@maven//:com_google_guava_guava",
        "@maven//:com_google_truth_truth",
        "@maven//:org_mockito_kotlin_mockito_kotlin",
        "@maven//:org_robolectric_robolectric",
        "@robolectric//bazel:android-all",
    ],
)

kt_android_local_test(
    name = "PaddleTest",
    srcs = ["PaddleTest.kt"],
    kotlinc_opts = ":kt_kotlinc_options",
    tags = ["robolectric"],
    deps = [
        "//java/com/google/androidenv/catch/sprite:Paddle",
        "//java/com/google/androidenv/catch/sprite:Point",
        "@maven//:androidx_test_ext_junit",
        "@maven//:com_google_guava_guava",
        "@maven//:com_google_truth_truth",
        "@maven//:org_mockito_kotlin_mockito_kotlin",
        "@maven//:org_robolectric_robolectric",
        "@robolectric//bazel:android-all",
    ],
)

kt_android_local_test(
    name = "SpriteTest",
    srcs = ["SpriteTest.kt"],
    kotlinc_opts = ":kt_kotlinc_options",
    deps = [
        "//java/com/google/androidenv/catch/sprite:Sprite",
        "@maven//:org_mockito_kotlin_mockito_kotlin",
        "@maven//:org_mockito_mockito_core",
    ],
)


================================================
FILE: android_env/apps/javatests/com/google/androidenv/catch/sprite/BackgroundTest.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch.sprite

import android.graphics.Canvas
import android.graphics.Color
import com.google.testing.junit.testparameterinjector.KotlinTestParameters.testValues
import com.google.testing.junit.testparameterinjector.TestParameter
import com.google.testing.junit.testparameterinjector.TestParameterInjector
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify

@RunWith(TestParameterInjector::class)
class BackgroundTest {

  @Test
  fun draw_defaultConstructorIsBlack() {
    // Arrange.
    val mockCanvas: Canvas = mock()
    val background: Background = Background()

    // Act.
    background.draw(mockCanvas)

    // Assert.
    verify(mockCanvas, times(1)).drawColor(Color.BLACK)
  }

  @Test
  fun draw_customColors(
    @TestParameter colorInt: Int = testValues(0, 255, 13_579, 2_468, 12_384_173)
  ) {
    // Arrange.
    val mockCanvas: Canvas = mock()
    val background: Background = Background(color = colorInt)

    // Act.
    background.draw(mockCanvas)

    // Assert.
    verify(mockCanvas, times(1)).drawColor(colorInt)
  }
}


================================================
FILE: android_env/apps/javatests/com/google/androidenv/catch/sprite/BallTest.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch.sprite

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import java.time.Duration
import kotlin.random.Random
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Suite
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.robolectric.ParameterizedRobolectricTestRunner

@RunWith(Suite::class)
@Suite.SuiteClasses(
  BallTest.UpdateAndResetTests::class,
  BallTest.ColorIntTest::class,
  BallTest.CheckBoundsTest::class,
  BallTest.IntersectsTest::class,
)
class BallTest {

  @RunWith(AndroidJUnit4::class)
  class UpdateAndResetTests() {
    @Test
    fun isOutOfBounds_initialState_isFalse() {
      // Arrange.
      val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 50 } // Horizontal middle.
      with(Ball(maxX = 100, maxY = 100, radius = 3.0f, speed = 1.0f, rand = mockRandom)) {
        assertThat(isOutOfBounds()).isEqualTo(false)
      }
    }

    @Test
    fun isOutOfBounds_initialState_isTrueIfRadiusExceedsMaxY() {
      // Arrange.
      val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 50 } // Horizontal middle.
      with(Ball(maxX = 100, maxY = 10, radius = 11.0f, speed = 1.0f, rand = mockRandom)) {
        assertThat(isOutOfBounds()).isEqualTo(true)
      }
    }

    @Test
    fun isOutOfBounds_initialState_isFalseIfRadiusExceedsOnlyMaxX() {
      // Arrange.
      val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 50 } // Horizontal middle.
      with(Ball(maxX = 10, maxY = 100, radius = 11.0f, speed = 1.0f, rand = mockRandom)) {
        assertThat(isOutOfBounds()).isEqualTo(false)
      }
    }

    @Test
    fun update_zeroDurationDoesNotMove_withinBounds() {
      // Arrange.
      val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 50 } // Horizontal middle.
      with(Ball(maxX = 100, maxY = 100, radius = 3.0f, speed = 1.0f, rand = mockRandom)) {
        // Act.
        update(Duration.ofMillis(0)) // The ball should not move.

        // Assert.
        assertThat(isOutOfBounds()).isEqualTo(false) // It should still be within the bounds.
      }
    }

    @Test
    fun update_zeroDurationDoesNotMove_outOfBounds() {
      // Arrange.
      val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 50 } // Horizontal middle.
      with(Ball(maxX = 100, maxY = 100, radius = 3.0f, speed = 1.0f, rand = mockRandom)) {
        update(Duration.ofMillis(110)) // Place the ball out of bounds.
        assertThat(isOutOfBounds()).isEqualTo(true)

        // Act.
        update(Duration.ofMillis(0)) // The ball should not move.

        // Assert.
        assertThat(isOutOfBounds()).isEqualTo(true) // It should still be out of bounds.
      }
    }

    @Test
    fun update_negativeDurationsMovesUp() {
      // Arrange.
      val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 50 } // Horizontal middle.
      with(Ball(maxX = 100, maxY = 100, radius = 3.0f, speed = 1.0f, rand = mockRandom)) {
        update(Duration.ofMillis(30)) // Move the ball down 30 pixels.
        assertThat(isOutOfBounds()).isEqualTo(false)

        // Act.
        update(Duration.ofMillis(-50)) // Move the ball _up_ 50 pixels.

        // Assert.
        assertThat(isOutOfBounds()).isEqualTo(true) // Now it should be out-of-bounds.
      }
    }

    @Test
    fun update_singleThrow() {
      // Ensures that a complete throw of a ball with radius==3.0f and maxY=100 behaves as expected.
      // [isOutOfBounds()] should return [false] for the first (100-3.0f-3.0f)=94 [update()] calls,
      // but [true] afterwards.

      // Arrange.
      val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 50 } // Horizontal middle.
      with(Ball(maxX = 100, maxY = 100, radius = 3.0f, speed = 1.0f, rand = mockRandom)) {
        // Act.
        repeat(94) {
          update(Duration.ofMillis(1))
          assertThat(isOutOfBounds()).isEqualTo(false)
        }
        update(Duration.ofMillis(1))

        // Assert.
        assertThat(isOutOfBounds()).isEqualTo(true)
      }
    }

    @Test
    fun intersects_afterUpdate() {
      // Arrange.
      val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 50 } // Horizontal middle.

      // Act & Assert.
      with(Ball(maxX = 100, maxY = 100, radius = 3.0f, speed = 1.0f, rand = mockRandom)) {
        assertThat(intersects(LineSegment(Point(40, 0), Point(60, 0)))).isEqualTo(true)
        update(Duration.ofMillis(1))
        assertThat(intersects(LineSegment(Point(40, 0), Point(60, 0)))).isEqualTo(false)
      }
    }

    @Test
    fun reset_intersectsInitialPositionShouldBeTrue() {
      // Arrange.
      val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 50 } // Horizontal middle.

      with(Ball(maxX = 100, maxY = 100, radius = 3.0f, speed = 1.0f, rand = mockRandom)) {
        // Act.
        assertThat(intersects(LineSegment(Point(40, 0), Point(60, 0)))).isEqualTo(true)

        update(Duration.ofMillis(1)) // Move the ball 1 pixels down.
        assertThat(intersects(LineSegment(Point(40, 0), Point(60, 0))))
          .isEqualTo(false) // Segment is now outside of the ball.

        reset() // Resetting should move the ball up again.

        // Assert.
        assertThat(intersects(LineSegment(Point(40, 0), Point(60, 0))))
          .isEqualTo(true) // Segment is now inside of the ball.
      }
    }

    @Test
    fun reset_differentInitialXCoordinates() {
      // Arrange.
      val ball: Ball = Ball(maxX = 100, maxY = 100, radius = 3.0f)

      // Act.
      var pointInside: Boolean = false
      var pointOutside: Boolean = false
      while (!pointInside || !pointOutside) {
        if (ball.intersects(LineSegment(Point(45, 0), Point(55, 0)))) {
          pointInside = true
        } else {
          pointOutside = true
        }
        ball.reset() // Sample a new initial position for the ball.
      }

      // Assert.
      // Eventually after many initial positions the ball should satisfy both conditions.
      assertThat(pointInside).isEqualTo(true)
      assertThat(pointOutside).isEqualTo(true)
    }
  }

  @RunWith(ParameterizedRobolectricTestRunner::class)
  class ColorIntTest(private val c: Int) {

    @Test
    fun draw_customBallColors() {
      // Arrange.
      val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 37 }
      val mockCanvas: Canvas = mock()
      val paintCaptor = argumentCaptor<Paint>()
      val ball: Ball = Ball(maxX = 50, maxY = 80, radius = 1.23f, color = c, rand = mockRandom)

      // Act.
      ball.draw(mockCanvas)

      // Assert.
      verify(mockCanvas).drawCircle(eq(37.0f), eq(2.0f), eq(1.23f), paintCaptor.capture())
      with(paintCaptor.lastValue) {
        assertThat(color).isEqualTo(c)
        assertThat(style).isEqualTo(Paint.Style.FILL)
      }
    }

    companion object {
      @JvmStatic
      @ParameterizedRobolectricTestRunner.Parameters(name = "color = {0}")
      fun parameters() = listOf(0, 255, -1, 13579, 2468, 12384173, Color.WHITE, Color.BLUE)
    }
  }

  @RunWith(ParameterizedRobolectricTestRunner::class)
  class CheckBoundsTest(private val p: ParamPack) {

    @Test
    fun intersects_checkBounds() {
      // Arrange.
      val mockRandom: Random =
        mock() { on { nextInt(any()) } doReturn p.maxX / 2 } // Horizontal middle.

      // Act.
      val ball: Ball = Ball(maxX = p.maxX, maxY = p.maxY, radius = p.radius, rand = mockRandom)

      // Assert.
      assertThat(ball.intersects(LineSegment(Point(p.x - 1, p.y), Point(p.x + 1, p.y))))
        .isEqualTo(p.expected)
    }

    data class ParamPack(
      val maxX: Int,
      val maxY: Int,
      val radius: Float,
      val x: Int,
      val y: Int,
      val expected: Boolean,
    )

    companion object {
      @JvmStatic
      @ParameterizedRobolectricTestRunner.Parameters(name = "param = {0}")
      fun parameters() =
        listOf(
          ParamPack(
            maxX = 100,
            maxY = 100,
            radius = 10.0f,
            x = 0,
            y = 0,
            expected = false,
          ), // Ball to the right of `x`.
          ParamPack(
            maxX = 100,
            maxY = 100,
            radius = 10.0f,
            x = 39,
            y = 0,
            expected = false,
          ), // Ball to the right of `x`.
          ParamPack(
            maxX = 100,
            maxY = 100,
            radius = 10.0f,
            x = 40,
            y = 10,
            expected = true,
          ), // Ball contains `x`.
          ParamPack(
            maxX = 100,
            maxY = 100,
            radius = 10.0f,
            x = 50,
            y = 0,
            expected = true,
          ), // Ball contains `x`.
          ParamPack(
            maxX = 100,
            maxY = 100,
            radius = 10.0f,
            x = 60,
            y = 10,
            expected = true,
          ), // Ball contains `x`.
          ParamPack(
            maxX = 100,
            maxY = 100,
            radius = 10.0f,
            x = 61,
            y = 0,
            expected = false,
          ), // Ball to the left of `x`.
          ParamPack(
            maxX = 100,
            maxY = 100,
            radius = 10.0f,
            x = 100,
            y = 0,
            expected = false,
          ), // Ball to the left of `x`.
          ParamPack(
            maxX = 100,
            maxY = 100,
            radius = 10.0f,
            x = 50,
            y = 21,
            expected = false,
          ), // Ball above `y`.
        )
    }
  }

  @RunWith(ParameterizedRobolectricTestRunner::class)
  class IntersectsTest(private val p: ParamPack) {

    @Test
    fun intersects_ballAtx50y10radius10() {
      // Arrange.
      val mockRandom: Random = mock() { on { nextInt(any()) } doReturn 50 } // Horizontal middle.

      // Act.
      val ball: Ball = Ball(maxX = 100, maxY = 100, radius = 10.0f, rand = mockRandom)

      // Assert.
      assertThat(ball.intersects(p.segment)).isEqualTo(p.expected)
    }

    data class ParamPack(val segment: LineSegment, val expected: Boolean)

    companion object {
      @JvmStatic
      @ParameterizedRobolectricTestRunner.Parameters(name = "param = {0}")
      fun parameters() =
        listOf(
          ParamPack(
            segment = LineSegment(Point(50, 10), Point(80, 40)),
            expected = true,
          ), // Segment that starts at the center of the ball so it should always intersect.
          ParamPack(
            segment = LineSegment(Point(49, 0), Point(51, 0)),
            expected = true,
          ), // Tangential segment that touches the bottom of the ball.
          ParamPack(
            segment = LineSegment(Point(40, 5), Point(65, 7)),
            expected = true,
          ), // Segment longer than diameter, touching the circumference twice.
          ParamPack(
            segment = LineSegment(Point(42, 2), Point(58, 1)),
            expected = true,
          ), // Segment shorter than diameter, touching the circumference twice.
          ParamPack(
            segment = LineSegment(Point(44, 4), Point(54, 3)),
            expected = true,
          ), // Segment shorter than diameter, fully inside the circle, not touching the
          // circumference.
          ParamPack(
            segment = LineSegment(Point(35, 4), Point(54, 3)),
            expected = true,
          ), // Segment that touches the circumference once "from the left".
          ParamPack(
            segment = LineSegment(Point(54, 7), Point(67, 13)),
            expected = true,
          ), // Segment that touches the circumference once "from the right".
          ParamPack(
            segment = LineSegment(Point(36, 7), Point(45, 0)),
            expected = false,
          ), // Segment "to the left of the ball". No intersection.
          ParamPack(
            segment = LineSegment(Point(58, -3), Point(60, 3)),
            expected = false,
          ), // Segment "to the right of the ball". No intersection.
        )
    }
  }
}


================================================
FILE: android_env/apps/javatests/com/google/androidenv/catch/sprite/PaddleTest.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch.sprite

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Suite
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.robolectric.ParameterizedRobolectricTestRunner

@RunWith(Suite::class)
@Suite.SuiteClasses(
  PaddleTest.ConstructorTests::class,
  PaddleTest.MoveTests::class,
  PaddleTest.XSetterTests::class,
  PaddleTest.DrawTests::class,
)
class PaddleTest {

  @RunWith(AndroidJUnit4::class)
  class ConstructorTests() {

    @Test
    fun x_initialValueShouldBeAtCenter() {
      with(Paddle(maxX = 30)) { assertThat(x).isEqualTo(15) }
      with(Paddle(maxX = 31)) { assertThat(x).isEqualTo(15) }
    }

    @Test
    fun topLeft_correspondsToGivenValues() {
      with(Paddle(width = 10, height = 6, maxX = 40, y = 33)) {
        assertThat(topLeft()).isEqualTo(Point(x = 15, y = 30))
      }
    }

    @Test
    fun topRight_correspondsToGivenValues() {
      with(Paddle(width = 10, height = 6, maxX = 40, y = 33)) {
        assertThat(topRight()).isEqualTo(Point(x = 25, y = 30))
      }
    }
  }

  @RunWith(ParameterizedRobolectricTestRunner::class)
  class MoveTests(private val p: ParamPack) {

    @Test
    fun move_expectedDestination() {
      // Arrange.
      with(Paddle(maxX = 50)) {
        // Act.
        move(deltaX = p.displacement)

        // Assert.
        assertThat(x).isEqualTo(p.expectedX)
      }
    }

    data class ParamPack(val displacement: Int, val expectedX: Int)

    companion object {
      @JvmStatic
      @ParameterizedRobolectricTestRunner.Parameters(name = "param = {0}")
      fun parameters() =
        listOf(
          // Initial position is x==25.
          ParamPack(displacement = 10, expectedX = 35),
          ParamPack(displacement = -10, expectedX = 15),
          ParamPack(displacement = 0, expectedX = 25),
          // Going beyond the left and right walls should clamp the values to 0 and 50.
          ParamPack(displacement = -26, expectedX = 0),
          ParamPack(displacement = 26, expectedX = 50),
        )
    }
  }

  @RunWith(ParameterizedRobolectricTestRunner::class)
  class XSetterTests(private val p: ParamPack) {

    @Test
    fun xSetter_expectedDestination() {
      // Arrange.
      with(Paddle(maxX = 50)) {
        // Act.
        x = p.target

        // Assert.
        assertThat(x).isEqualTo(p.expectedX)
      }
    }

    data class ParamPack(val target: Int, val expectedX: Int)

    companion object {
      @JvmStatic
      @ParameterizedRobolectricTestRunner.Parameters(name = "param = {0}")
      fun parameters() =
        listOf(
          // Initial position is x==25.
          ParamPack(target = 0, expectedX = 0),
          ParamPack(target = 15, expectedX = 15),
          ParamPack(target = 25, expectedX = 25),
          ParamPack(target = 35, expectedX = 35),
          ParamPack(target = 50, expectedX = 50),
          // Going beyond the left and right walls should clamp the values to 0 and 50.
          ParamPack(target = -1, expectedX = 0),
          ParamPack(target = 51, expectedX = 50),
        )
    }
  }

  @RunWith(AndroidJUnit4::class)
  class DrawTests() {

    @Test
    fun draw_initialPosition() {
      // Arrange.
      val mockCanvas: Canvas = mock()
      val rectCaptor = argumentCaptor<Rect>()
      val paintCaptor = argumentCaptor<Paint>()
      with(Paddle(color = Color.RED, width = 100, height = 20, maxX = 300, y = 400)) {
        // Act.
        draw(mockCanvas)

        // Assert.
        assertThat(x).isEqualTo(150)
        verify(mockCanvas).drawRect(rectCaptor.capture(), paintCaptor.capture())
        with(rectCaptor.lastValue) {
          assertThat(bottom).isEqualTo(400 + 10)
          assertThat(top).isEqualTo(400 - 10)
          assertThat(left).isEqualTo(150 - 50)
          assertThat(right).isEqualTo(150 + 50)
        }
      }
    }

    @Test
    fun draw_afterMove() {
      // Arrange.
      val mockCanvas: Canvas = mock()
      val rectCaptor = argumentCaptor<Rect>()
      val paintCaptor = argumentCaptor<Paint>()
      with(Paddle(color = Color.RED, width = 100, height = 20, maxX = 300, y = 400)) {
        // Act.
        move(50)
        draw(mockCanvas)

        // Assert.
        assertThat(x).isEqualTo(200)
        verify(mockCanvas).drawRect(rectCaptor.capture(), paintCaptor.capture())
        with(rectCaptor.lastValue) {
          assertThat(bottom).isEqualTo(400 + 10)
          assertThat(top).isEqualTo(400 - 10)
          assertThat(left).isEqualTo(200 - 50)
          assertThat(right).isEqualTo(200 + 50)
        }
        with(paintCaptor.lastValue) {
          assertThat(color).isEqualTo(Color.RED)
          assertThat(style).isEqualTo(Paint.Style.FILL)
        }
      }
    }
  }
}


================================================
FILE: android_env/apps/javatests/com/google/androidenv/catch/sprite/SpriteTest.kt
================================================
// Copyright 2026 DeepMind Technologies Limited.
//
// 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.

package com.google.androidenv.catch.sprite

import android.graphics.Canvas
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mockito.verifyNoInteractions
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify

/** Trivial tests to ensure the types in the API are correct. */
@RunWith(JUnit4::class)
class SpriteTest {

  @Test
  fun defaultImplementationDoesNothing() {
    // Arrange.
    val mockCanvas: Canvas = mock()
    val sprite = Sprite()

    // Act.
    sprite.draw(mockCanvas)

    // Assert.
    verifyNoInteractions(mockCanvas) // No methods should be called on the canvas.
  }

  @Test
  fun draw_argumentsAreForwarded() {
    // Arrange.
    val mockSprite: Sprite = mock()
    val mockCanvas: Canvas = mock()

    // Act.
    mockSprite.draw(mockCanvas)

    // Assert.
    verify(mockSprite, times(1)).draw(mockCanvas)
  }
}


================================================
FILE: android_env/components/__init__.py
================================================
# coding=utf-8
# Copyright 2026 DeepMind Technologies Limited.
#
# 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.



================================================
FILE: android_env/components/action_fns.py
================================================
# coding=utf-8
# Copyright 2026 DeepMind Technologies Limited.
#
# 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.

"""Functions to convert actions between different components' formats."""

from absl import logging
from android_env.components import action_type as action_type_lib
from android_env.components import errors
from android_env.components import pixel_fns
from android_env.components.simulators import base_simulator
import numpy as np


def send_action_to_simulator(
    action: dict[str, np.ndarray],
    simulator: base_simulator.BaseSimulator,
    screen_width: int,
    screen_height: int,
    num_fingers: int,
) -> bool:
  """Sends the selected action to the given simulator.

  The simulator will interpret the action according to `action["action_type"]`.
  The effect this action triggers in the Android OS will be determined by the
  currently running application.

  Args:
    action: action which will get interpreted as a touchscreen event.
    simulator: The simulator that will receive the action.
    screen_width: The width of the touchscreen in pixels.
    screen_height: The height of the touchscreen in pixels.
    num_fingers: The number of fingers used in this simulator.
  """

  try:
    match action['action_type']:
      # If the action is a TOUCH or LIFT, send a touch event to the simulator.
      case action_type_lib.ActionType.TOUCH | action_type_lib.ActionType.LIFT:
        prepared_action = _prepare_touch_action(
            action, screen_width, screen_height, num_fingers
        )
        simulator.send_touch(prepared_action)
      # If the action is a key event, send a key event to the simulator.
      case action_type_lib.ActionType.KEYDOWN:
        simulator.send_key(action['keycode'].item(0), event_type='keydown')
      case action_type_lib.ActionType.KEYUP:
        simulator.send_key(action['keycode'].item(0), event_type='keyup')
      case action_type_lib.ActionType.KEYPRESS:
        simulator.send_key(action['keycode'].item(0), event_type='keypress')
  except errors.SendActionError:
    logging.exception('Unable to execute action: %r', action)
    return False

  return True


def _prepare_touch_action(
    action: dict[str, np.ndarray],
    screen_width: int,
    screen_height: int,
    num_fingers: int,
) -> list[tuple[int, int, bool, int]]:
  """Turns an AndroidEnv action into values that the simulator can interpret.

  Converts float-valued 'touch_position' to integer coordinates corresponding
  to specific pixels, and 'action_type' to booleans indicating whether the
  screen is touched at said location or not. The result of this function can
  be sent directly to the underlying simulator (e.g. the Android Emulator,
  virtual machine, or a phone).

  Args:
    action: An action containing 'action_type' and 'touch_position'.

  Returns:
    A tuple with the format (x: int, y: int, down/up: bool, finger_index: int).
  """

  touch_events = []
  for i, finger_action in enumerate(_split_touch_action(action, num_fingers)):
    is_touch = finger_action['action_type'] == action_type_lib.ActionType.TOUCH
    touch_position = finger_action['touch_position']
    touch_pixels = pixel_fns.touch_position_to_pixel_position(
        touch_position, width_height=(screen_width, screen_height)
    )
    touch_events.append((touch_pixels[0], touch_pixels[1], is_touch, i))
  return touch_events


def _split_touch_action(
    action: dict[str, np.ndarray], num_fingers: int
) -> list[dict[str, np.ndarray]]:
  """Splits a multitouch action into a list of single-touch actions."""

  single_touch_actions = [{
      'action_type': action['action_type'],
      'touch_position': action['touch_position'],
  }]
  for i in range(2, num_fingers + 1):
    single_touch_actions.append({
        'action_type': action[f'action_type_{i}'],
        'touch_position': action[f'touch_position_{i}'],
    })
  return single_touch_actions


def lift_all_fingers_action(num_fingers: int) -> dict[str, np.ndarray]:
  """A lift action with each finger."""

  # There's always at least one finger.
  lift_action = {
      'action_type': np.array(action_type_lib.ActionType.LIFT),
      'touch_position': np.array([0, 0]),
  }
  # Subsequent fingers have separate dict entries.
  for i in range(2, num_fingers + 1):
    lift_action |= {
        f'action_type_{i}': np.array(action_type_lib.ActionType.LIFT),
        f'touch_position_{i}': np.array([0, 0]),
    }
  return lift_action


================================================
FILE: android_env/components/action_fns_test.py
================================================
# coding=utf-8
# Copyright 2026 DeepMind Technologies Limited.
#
# 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.

from unittest import mock

from absl.testing import absltest
from absl.testing import parameterized
from android_env.components import action_fns
from android_env.components import action_type as action_type_lib
from android_env.components import errors
from android_env.components.simulators import base_simulator
import numpy as np


class ActionFnsTest(parameterized.TestCase):

  def test_send_action_to_simulator_missing_action_type(self):
    """A `KeyError` should be raised if the action is missing "action_type"."""

    # Arrange.
    simulator = mock.create_autospec(base_simulator.BaseSimulator)
    action = {'some_key': np.array(123, np.int32)}

    # Act & Assert.
    self.assertRaises(
        KeyError,
        action_fns.send_action_to_simulator,
        action,
        simulator,
        800,
        600,
        1,
    )

  def test_send_action_to_simulator_sendactionerror(self):
    """Returns `False` if the simulator raises a SendActionError."""

    # Arrange.
    simulator = mock.create_autospec(base_simulator.BaseSimulator)
    simulator.send_touch.side_effect = errors.SendActionError('oops!')
    action = {
        'action_type': action_type_lib.ActionType.TOUCH,
        'touch_position': np.array([0.3, 0.5], np.float32),
    }

    # Act.
    output = action_fns.send_action_to_simulator(
        action,
        simulator,
        800,
        600,
        1,
    )

    # Assert.
    self.assertFalse(output)
    simulator.send_touch.assert_called_once()

  def test_send_action_to_simulator_touch_success_one_finger(self):
    """Returns `True` with a proper 1-finger touch action."""

    # Arrange.
    simulator = mock.create_autospec(base_simulator.BaseSimulator)
    action = {
        'action_type': action_type_lib.ActionType.TOUCH,
        'touch_position': np.array([0.2, 0.5], np.float32),
    }

    # Act.
    output = action_fns.send_action_to_simulator(
        action,
        simulator,
        800,
        600,
        1,
    )

    # Assert.
    self.assertTrue(output)
    simulator.send_touch.assert_called_once_with(
        [(np.int32(160), np.int32(300), True, 0)]
    )

  def test_send_action_to_simulator_touch_success_multiple_finger(self):
    """Returns `True` with a proper 3-finger touch action."""

    # Arrange.
    simulator = mock.create_autospec(base_simulator.BaseSimulator)
    action = {
        'action_type': action_type_lib.ActionType.TOUCH,
        'touch_position': np.array([0.2, 0.5], np.float32),
        'action_type_2': action_type_lib.ActionType.LIFT,
        'touch_position_2': np.array([0.1, 0.2], np.float32),
        'action_type_3': action_type_lib.ActionType.TOUCH,
        'touch_position_3': np.array([0.5, 0.2], np.float32),
    }

    # Act.
    output = action_fns.send_action_to_simulator(
        action,
        simulator,
        800,
        600,
        3,
    )

    # Assert.
    self.assertTrue(output)
    simulator.send_touch.assert_called_once_with([
        (np.int32(160), np.int32(300), True, 0),
        (np.int32(80), np.int32(120), False, 1),
        (np.int32(400), np.int32(120), True, 2),
    ])

  def test_send_action_to_simulator_keydown_success(self):
    """Returns `True` with a proper keydown action."""

    # Arrange.
    simulator = mock.create_autospec(base_simulator.BaseSimulator)
    action = {
        'action_type': action_type_lib.ActionType.KEYDOWN,
        'keycode': np.array([21], np.int32),
    }

    # Act.
    output = action_fns.send_action_to_simulator(
        action,
        simulator,
        800,
        600,
        1,
    )

    # Assert.
    self.assertTrue(output)
    simulator.send_key.assert_called_once_with(21, event_type='keydown')

  def test_send_action_to_simulator_keyup_success(self):
    """Returns `True` with a proper keyup action."""

    # Arrange.
    simulator = mock.create_autospec(base_simulator.BaseSimulator)
    action = {
        'action_type': action_type_lib.ActionType.KEYUP,
        'keycode': np.array([42], np.int32),
    }

    # Act.
    output = action_fns.send_action_to_simulator(
        action,
        simulator,
        800,
        600,
        1,
    )

    # Assert.
    self.assertTrue(output)
    simulator.send_key.assert_called_once_with(42, event_type='keyup')

  def test_send_action_to_simulator_keypress_success(self):
    """Returns `True` with a proper keypress action."""

    # Arrange.
    simulator = mock.create_autospec(base_simulator.BaseSimulator)
    action = {
        'action_type': action_type_lib.ActionType.KEYPRESS,
        'keycode': np.array([96], np.int32),
    }

    # Act.
    output = action_fns.send_action_to_simulator(
        action,
        simulator,
        800,
        600,
        1,
    )

    # Assert.
    self.assertTrue(output)
    simulator.send_key.assert_called_once_with(96, event_type='keypress')

  @parameterized.named_parameters(
      (
          'one_finger',
          1,
          {
              'action_type': np.array(action_type_lib.ActionType.LIFT),
              'touch_position': np.array([0, 0]),
          },
      ),
      (
          'two_fingers',
          2,
          {
              'action_type': np.array(action_type_lib.ActionType.LIFT),
              'touch_position': np.array([0, 0]),
              'action_type_2': np.array(action_type_lib.ActionType.LIFT),
              'touch_position_2': np.array([0, 0]),
          },
      ),
  )
  def test_lift_all_fingers_action(
      self, num_fingers: int, expected_action: dict[str, np.ndarray]
  ):
    """Returns the expected action."""

    output = action_fns.lift_all_fingers_action(num_fingers)
    for k, v in expected_action.items():
      np.testing.assert_array_equal(v, output[k])


if __name__ == '__main__':
  absltest.main()


================================================
FILE: android_env/components/action_type.py
================================================
# coding=utf-8
# Copyright 2026 DeepMind Technologies Limited.
#
# 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.

"""The different kinds of actions that AndroidEnv supports.

The native action space of AndroidEnv consists of a tuple consisting of
- A position (x, y) ∈ [0, 1] x [0, 1], determining the location of the action on
  the screen, and
- A discrete value, indicating the action type, which is in this file.

See https://arxiv.org/abs/2105.13231, section 2.2 for details.
"""

import enum


@enum.unique
class ActionType(enum.IntEnum):
  """Integer values to describe each supported action in AndroidEnv.

  Note for KEY* types:
  - Only meaningful if connected to a _physical_ keyboard, _not_ virtual
    keyboard.
  - Added afterwards so they did not appear in the paper.

  Attributes:
    TOUCH: Touching the screen at a location.
    LIFE: Lifting the (imaginary) pointer from the screen at a location.
    REPEAT: Repeating the last chosen action.
    KEYDOWN: Sending a key down event.
    KEYUP: Sending a key up event.
    KEYPRESS: Sending a key down event, immediately followed by a key up event.
  """

  TOUCH = 0
  LIFT = 1
  REPEAT = 2
  KEYDOWN = 3
  KEYUP = 4
  KEYPRESS = 5


================================================
FILE: android_env/components/adb_call_parser.py
================================================
# coding=utf-8
# Copyright 2026 DeepMind Technologies Limited.
#
# 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.

"""Processes adb_pb2.AdbRequest commands."""

import os
import re
import subprocess
import sys
import tempfile

from absl import logging
from android_env.components import adb_controller as adb_control
from android_env.proto import adb_pb2

# A mapping from a Button enum to keycode strings.
#
# Please see https://developer.android.com/reference/android/view/KeyEvent
#
# We currently only accept the following entries:
_BUTTON_TO_KEYCODE = {
    adb_pb2.AdbRequest.PressButton.Button.HOME: 'KEYCODE_HOME',
    adb_pb2.AdbRequest.PressButton.Button.BACK: 'KEYCODE_BACK',
    adb_pb2.AdbRequest.PressButton.Button.ENTER: 'KEYCODE_ENTER',
}


class AdbCallParser:
  """Parses AdbRequest messages and executes corresponding adb commands."""

  def __init__(self, adb_controller: adb_control.AdbController):
    self._adb_controller = adb_controller
    self._handlers = {
        'install_apk': self._install_apk,
        'start_activity': self._start_activity,
        'force_stop': self._force_stop,
        'tap': self._tap,
        'press_button': self._press_button,
        'start_screen_pinning': self._start_screen_pinning,
        'send_broadcast': self._send_broadcast,
        'uninstall_package': self._handle_uninstall_package,
        'get_current_activity': self._get_current_activity,
        'get_orientation': self._get_orientation,
        'push': self._push,
        'pull': self._pull,
        'input_text': self._input_text,
        'settings': self._handle_settings,
        'generic': self._handle_generic,
        'package_manager': self._handle_package_manager,
        'dumpsys': self._handle_dumpsys,
    }

  def _execute_command(
      self, command_args: list[str], timeout: float | None
  ) -> tuple[adb_pb2.AdbResponse, bytes]:
    """Executes the command, catches errors and populates the response status.

    Args:
      command_args: a list of arguments for the ADB request.
      timeout: Timeout in seconds.

    Returns:
      A tuple of the AdbResponse with the status populated, and the output
      bytes from the command.
    """
    response = adb_pb2.AdbResponse(status=adb_pb2.AdbResponse.Status.OK)
    command_output = b''
    try:
      command_output = self._adb_controller.execute_command(
          command_args, timeout=timeout)
    except subprocess.CalledProcessError as adb_error:
      if adb_error.stdout is not None:
        response.status = adb_pb2.AdbResponse.Status.ADB_ERROR
        response.error_message = adb_error.stdout
    except subprocess.TimeoutExpired:
      response.status = adb_pb2.AdbResponse.Status.TIMEOUT
      response.error_message = 'Timeout'

    return response, command_output

  def parse(self, request: adb_pb2.AdbRequest) -> adb_pb2.AdbResponse:
    """Executes `request` and returns an appropriate response."""

    response = adb_pb2.AdbResponse(status=adb_pb2.AdbResponse.Status.OK)
    command_type = request.WhichOneof('command')
    logging.debug('AdbRequest command type: %s', command_type)
    if command_type is None:
      response.status = adb_pb2.AdbResponse.Status.UNKNOWN_COMMAND
      response.error_message = 'AdbRequest.command is None.'
      return response

    if request.timeout_sec < 0:
      response.status = adb_pb2.AdbResponse.Status.FAILED_PRECONDITION
      response.error_message = ('AdbRequest.timeout_sec cannot be negative. '
                                f'Got: {request.timeout_sec}')
      return response

    timeout: float | None = request.timeout_sec or None
    return self._handlers[command_type](request, timeout)

  def _force_stop(
      self, request: adb_pb2.AdbRequest, timeout: float | None = None
  ) -> adb_pb2.AdbResponse:
    """Stops an application.

    Args:
      request: The external request containing the package to force stop.
      timeout: Optional time limit in seconds.

    Returns:
      An AdbResponse.
    """

    force_stop = request.force_stop
    response = adb_pb2.AdbResponse(status=adb_pb2.AdbResponse.Status.OK)
    if not force_stop.package_name:
      response.status = adb_pb2.AdbResponse.Status.FAILED_PRECONDITION
      response.error_message = '`force_stop.package_name` cannot be empty.'
      return response

    response, _ = self._execute_command(
        ['shell', 'am', 'force-stop', force_stop.package_name], timeout)

    return response

  def _fetch_current_task_id(
      self, full_activity_name: str, timeout: float | None = None
  ) -> int:
    """Returns the task ID of the given `full_activity_name`.

    Args:
      full_activity_name: The full name of the activity whose corresponding
        task id we are looking for.
      timeout: Optional time limit in seconds.
    Returns:
      task_id: An integer corresponding to the specified activity.
    """

    stack = self._adb_controller.execute_command(
        ['shell', 'am', 'stack', 'list'], timeout=timeout)
    lines = stack.decode('utf-8').splitlines()

    regex = re.compile(
        r'^\ *taskId=(?P<id>[0-9]*): (?P<base_activity>[^\s]*) .*visible=true'
        r'.*topActivity=ComponentInfo{(?P<top_activity>[^\s]*)}$')

    for line in lines:
      match = regex.search(line)
      if match is None:
        continue

      current_task_id_str = match.group('id')
      base_activity = match.group('base_activity')
      top_activity = match.group('top_activity')

      # If neither of the matched activities equals the activity we are
      # looking for, we discard their task id and continue the search.
      if full_activity_name not in {base_activity, top_activity}:
        logging.info('Full activity %s was not found in current line %s',
                     full_activity_name, line)
        continue

      # Otherwise return the integer task id.
      try:
        return int(current_task_id_str)
      except ValueError:
        logging.info('Failed to parse task ID [%r].', current_task_id_str)

    # At this point if we could not find a task ID, there's nothing we can do.
    logging.error('Could not find current activity in stack list: %r', lines)
    return -1

  def _start_screen_pinning(
      self, request: adb_pb2.AdbRequest, timeout: float | None = None
  ) -> adb_pb2.AdbResponse:
    """Pins an application.

    Args:
      request: The request containing the activity to pin.
      timeout: Optional time limit in seconds.

    Returns:
      An AdbResponse.
    """

    full_activity = request.start_screen_pinning.full_activity
    response = adb_pb2.AdbResponse(status=adb_pb2.AdbResponse.Status.OK)
    if not full_activity:
      response.status = adb_pb2.AdbResponse.Status.FAILED_PRECONDITION
      response.error_message = (
          '`start_screen_pinning.full_activity` cannot be empty.')
      return response

    current_task_id = self._fetch_current_task_id(full_activity, timeout)
    if current_task_id == -1:
      response.status = adb_pb2.AdbResponse.Status.INTERNAL_ERROR
      response.error_message = ('Could not find task ID for activity '
                                f'[{full_activity}]')
      return response

    response, _ = self._execute_command(
        ['shell', 'am', 'task', 'lock',
         str(current_task_id)], timeout=timeout)

    return response

  def _send_broadcast(
      self, request: adb_pb2.AdbRequest, timeout: float | None = None
  ) -> adb_pb2.AdbResponse:
    """Sends a broadcast.

    Args:
      request: The request with the information for the broadcast event.
      timeout: Optional time limit in seconds.

    Returns:
      An AdbResponse.
    """

    send_broadcast = request.send_broadcast
    response = adb_pb2.AdbResponse(status=adb_pb2.AdbResponse.Status.OK)
    if not send_broadcast.action:
      response.status = adb_pb2.AdbResponse.Status.FAILED_PRECONDITION
      response.error_message = ('`send_broadcast.{action}` cannot be empty.')
      return response

    if send_broadcast.component:
      component_args = ['-n', send_broadcast.component]
    else:
      component_args = []

    response, _ = self._execute_command(
        ['shell', 'am', 'broadcast', '-a', send_broadcast.action]
        + component_args,
        timeout=timeout,
    )

    return response

  def _install_apk(
      self, request: adb_pb2.AdbRequest, timeout: float | None = None
  ) -> adb_pb2.AdbResponse:
    """Installs an app given its local path in the filesystem.

    Args:
      request: The external request with an install_apk field.
        Contains information for the .apk installation.
      timeout: Optional time limit in seconds.

    Returns:
      An AdbResponse.
    """

    install_apk = request.install_apk
    response = adb_pb2.AdbResponse()
    location_type = install_apk.WhichOneof('location')
    logging.info('location_type: %s', location_type)

    match location_type:
      case 'filesystem':
        fpath = install_apk.filesystem.path
        if not os.path.exists(fpath):
          response.status = adb_pb2.AdbResponse.Status.INTERNAL_ERROR
          response.error_message = f'Could not find local_apk_path: {fpath}'
          return response

        response, _ = self._execute_command(
            ['install', '-r', '-t', '-g', fpath], timeout=timeout
        )
      case 'blob':

        # `delete_on_close` was only added in Python 3.12 so we add a switch
        # here to still support previous Python versions.
        if sys.version_info >= (3, 12):
          kwargs = {'suffix': '.apk', 'delete_on_close': False}
        else:
          kwargs = {'suffix': '.apk'}

        with tempfile.NamedTemporaryFile(**kwargs) as f:
          fpath = f.name
          f.write(install_apk.blob.contents)

          response, _ = self._execute_command(
              ['install', '-r', '-t', '-g', fpath], timeout=timeout
          )
      case _:
        response.status = adb_pb2.AdbResponse.Status.FAILED_PRECONDITION
        response.error_message = (
            f'Unsupported `install_apk.location` type: {location_type}'
        )
        return response

    return response

  def _start_activity(
      self, request: adb_pb2.AdbRequest, timeout: float | None = None
  ) -> adb_pb2.AdbResponse:
    """Starts a given activity.

    Options for `start_activity`:
      `am start` command options:
      -D: enable debugging
      -W: wait for launch to complete
      --start-profiler <FILE>: start profiler and send results to <FILE>
      -P <FILE>: like above, but profiling stops when app goes idle
      -R: repeat the activity launch <COUNT> times.  Prior to each repeat,
          the top activity will be finished.
      -S: force stop the target app before starting the activity
      --opengl-trace: enable tracing of OpenGL functions

    Args:
      request: The request with information on what activity to start.
      timeout: Optional time limit in seconds.

    Returns:
      An AdbResponse. If successful, StartActivityResponse will contain the
      activity name and adb command output.
    """

    activity = request.start_activity.full_activity
    if not activity:
      return adb_pb2.AdbResponse(
          status=adb_pb2.AdbResponse.Status.FAILED_PRECONDITION,
          error_message='`start_activity.full_activity` cannot be empty.')

    force_stop = '-S' if request.start_activity.force_stop else ''
    response, command_output = self._execute_command(
        ['shell', 'am', 'start', force_stop, '-W', '-n', activity] +
        list(request.start_activity.extra_args or []),
        timeout=timeout)

    # Check command output for potential errors.
    expected_error = re.compile(r""".*Error.*""", re.VERBOSE)
    if expected_error.match(str(command_output)):
      return adb_pb2.AdbResponse(
          status=adb_pb2.AdbResponse.Status.INTERNAL_ERROR,
          error_message=f'start_activity failed with error: {command_output}')

    response.start_activity.full_activity = activity
    response.start_activity.output = command_output
    return response

  def _press_button(
      self, request: adb_pb2.AdbRequest, timeout: float | None = None
  ) -> adb_pb2.AdbResponse:
    """Presses a keyboard key.

    Args:
      request: The request with information on what button to press.
      timeout: Optional time limit in seconds.

    Returns:
      An AdbResponse.
    """

    button = request.press_button.button
    if button not in _BUTTON_TO_KEYCODE:
      return adb_pb2.AdbResponse(
          status=adb_pb2.AdbResponse.Status.FAILED_PRECONDITION,
          error_message=('PressButton.button must be one of '
                         f'[{_BUTTON_TO_KEYCODE.keys()}]. '
                         f'Got: {button}. Please see `adb.proto`.'))

    keycode = _BUTTON_TO_KEYCODE[button]
    response, command_output = self._execute_command(
        ['shell', 'input', 'keyevent', keycode], timeout=timeout)
    response.press_button.output = command_output
    return response

  def _handle_uninstall_package(
      self, request: adb_pb2.AdbRequest, timeout: float | None = None
  ) -> adb_pb2.AdbResponse:
    """Handles UninstallPackage messages.

    Args:
      request: The specification of what to uninstall.
      timeout: Optional time limit in seconds.

    Returns:
      An AdbResponse
    """

    package_name = request.uninstall_package.package_name
    response = adb_pb2.AdbResponse()
    # Every UninstallPackage should have a package_name.
    if not package_name:
      response.status = adb_pb2.AdbResponse.Status.FAILED_PRECONDITION
      response.error_message = (
          '`uninstall_package.package_name` cannot be empty.')
      return response

    # Get list of installed packages and issue an uninstall only if it's
    # already installed.
    package_response = self._handle_package_manager(
        adb_pb2.AdbRequest(
            package_manager=adb_pb2.AdbRequest.PackageManagerRequest(
                list=adb_pb2.AdbRequest.PackageManagerRequest.List(
                    packages=adb_pb2.AdbRequest.PackageManagerRequest.List
                    .Packages()))))
    if package_name in package_response.package_manager.list.items:
      response, _ = self._execute_command(['uninstall', package_name], timeout)
    else:
      msg = (f'Cannot uninstall {package_name} since it is not installed.')
      logging.warning(msg)
      response.error_message = msg

    return response

  def _get_current_activity(
      self, request: adb_pb2.AdbRequest, timeout: float | None = None
  ) -> adb_pb2.AdbResponse:
    """Fetches current activity.

    Args:
      request: The request with the `.get_current_activity` field set. This is
        unused, but it's in the signature so that all calls are uniform.
      timeout: Optional time limit in seconds.

    Returns:
      AdbResponse containing the current activity.
    """

    del request  # Unused.

    response, visible_task = self._execute_command(
        ['shell', 'am', 'stack', 'list', '|', 'grep', '-E', 'visible=true'],
        timeout=timeout)

    if response.status != adb_pb2.AdbResponse.Status.OK:
      return response

    if not visible_task:
      _, am_stack_list = self._execute_command(['shell', 'am', 'stack', 'list'],
                                               timeout=timeout)
      response.status = adb_pb2.AdbResponse.Status.INTERNAL_ERROR
      response.error_message = ('Empty visible_task. `am stack list`: '
                                f'{am_stack_list}')
      return response

    visible_task = visible_task.decode('utf-8')
    if sys.platform == 'win32':
      visible_task_list = re.findall(
          r'visible=true topActivity=ComponentInfo{(.+?)}', visible_task)
      if not visible_task_list:
        visible_task = ''
      else:
        visible_task = 'ComponentInfo{' + visible_task_list[0] + '}'

    p = re.compile(r'.*\{(.*)\}')
    matches = p.search(visible_task)
    if matches is None:
      _, am_stack_list = self._execute_command(['shell', 'am', 'stack', 'list'],
                                               timeout=timeout)
      response.status = adb_pb2.AdbResponse.Status.INTERNAL_ERROR
      response.error_message = (
          'Could not extract current activity. Will return nothing. '
          f'`am stack list`: {am_stack_list}')
      return response

    response.get_current_activity.full_activity = matches.group(1)
    return response

  def _get_orientation(
      self, request: adb_pb2.AdbRequest, timeout: float | None = None
  ) -> adb_pb2.AdbResponse:
    """Fetches current device orientation.

    Args:
      request: The request with the `.get_orientation` field set.
      timeout: Optional time limit in seconds.

    Returns:
      AdbResponse containing the current device orientation. This is
          unused, but it's in the signature so that all calls are uniform.
    """

    del request  # Unused.

    logging.info('Getting orientation...')
    response = self._handle_dumpsys(
        adb_pb2.AdbRequest(
            dumpsys=adb_pb2.AdbRequest.DumpsysRequest(service='input')),
        timeout=timeout)
    output = response.dumpsys.output
    if not output:
      logging.error('Empty dumpsys output.')
      response.status = adb_pb2.AdbResponse.Status.INTERNAL_ERROR
      response.error_message = 'Failed to execute `dumpsys input`'
      return response

    output = output.decode('utf-8')
    lines = output.split('\n')  # Split by lines.
    skip_next = False
    for line in lines:
      # There may be multiple devices in output. An invalid device can be
      # identified by negative PhysicalWidth.
      physical_width = re.match(r'\s+PhysicalWidth:\s+(-?\d+)px', line)
      if physical_width:
        skip_next = int(physical_width.group(1)) < 0
      # Depending on the device type, the orientation could take these forms:
      # SurfaceOrientation: 0
      # InputDeviceOrientation: Rotation0
      surface_orientation = re.match(
          r'\s+(SurfaceOrientation|InputDeviceOrientation):\s+.*(\d)', line
      )

      if surface_orientation is not None:
        if skip_next:
          continue
        if surface_orientation.re.groups < 2:
          continue
        orientation = surface_orientation.group(2)
        logging.info('Done getting orientation: %r', orientation)
        response.get_orientation.orientation = int(orientation)
        return response

    response.status = adb_pb2.AdbResponse.Status.INTERNAL_ERROR
    response.error_message = (
        'Could not find SurfaceOrientation/InputDeviceOrientation in dumpsys '
        'output'
    )
    return response

  def _push(
      self, request: adb_pb2.AdbRequest, timeout: float | None = None
  ) -> adb_pb2.AdbResponse:
    """Uploads contents to the device.

    Args:
      request: The request with the contents to push to the device.
      timeout: Optional time limit in seconds.

    Returns:
      An empty AdbResponse.
    """

    path = request.push.path
    if not path:
      return adb_pb2.AdbResponse(
          status=adb_pb2.AdbResponse.Status.FAILED_PRECONDITION,
          error_message='Push.path is empty.')

    # Create temporary file with `push` contents.
    with tempfile.NamedTemporaryFile(delete=False) as f:
      fname = f.name
      f.write(request.push.content)
    # Issue `adb push` command to upload file.
    logging.info('Uploading %r to %r.', fname, path)
    response, _ = self._execute_command(['push', fname, path], timeout=timeout)
    # Delete it.
    os.remove(fname)

    return response

  def _pull(
      self, request: adb_pb2.AdbRequest, timeout: float | None = None
  ) -> adb_pb2.AdbResponse:
    """Downloads file content from the device.

    Args:
      request: The request with the information on what to get from the device.
      timeout: Optional time limit in seconds.

    Returns:
      An AdbResponse with the contents of the specified file.
    """

    path = request.pull.path
    if not path:
      return adb_pb2.AdbResponse(
          status=adb_pb2.AdbResponse.Status.FAILED_PRECONDITION,
          error_message='Pull.path is empty.')

    # Issue `adb pull` command to copy it to a temporary file.
    with tempfile.NamedTemporaryFile(delete=False) as f:
      fname = f.name
      logging.debug('Downloading %r to %r.', path, fname)
      response, _ = self._execute_command(['pull', path, fname],
                                          timeout=timeout)
    # Read the content of the file.
    with open(fname, 'rb') as f:
      response.pull.content = f.read()
    # Delete it.
    os.remove(fname)

    return response

  def _input_text(
      self, request: adb_pb2.AdbRequest, timeout: float | None = None
  ) -> adb_pb2.AdbResponse:
    """Inserts text as keyboard events.

    Args:
      request: The external request.
      timeout: Optional time limit in seconds.

    Returns:
      An AdbResponse
    """

    text = request.input_text.text
    if not text:
      return adb_pb2.AdbResponse(
          status=adb_pb2.AdbResponse.Status.FAILED_PRECONDITION,
          error_message='InputText.text is empty.')

    response, _ = self._execute_command(['shell', 'input', 'text', text],
                                        timeout=timeout)
    return response

  def _tap(
      self, request: adb_pb2.AdbRequest, timeout: float | None = None
  ) -> adb_pb2.AdbResponse:
    """Taps the device screen.

    Args:
      request: The request with information on where to tap the screen.
      timeout: Optional time limit in seconds.

    Returns:
      An AdbResponse
    """

    x = request.tap.x
    y = request.tap.y
    # Check for negative coordinates.
    # Notice that zero coordinates are valid coordinates (i.e. the first
    # column/row of the screen).
    if x < 0 or y < 0:
      return adb_pb2.AdbResponse(
          status=adb_pb2.AdbResponse.Status.FAILED_PRECONDITION,
          error_message=(
              f'Tap coordinates must be non-negative. Got: {request.tap}.'))

    response, _ = self._execute_command(
        ['shell', 'input', 'tap', str(x),
         str(y)], timeout=timeout)

    return response

  def _handle_settings(
      self, request: adb_pb2.AdbRequest, timeout: float | None = None
  ) -> adb_pb2.AdbResponse:
    """Handles SettingsRequest messages.

    Args:
      request: The specification of what to do with settings.
      timeout: Optional time limit in seconds.

    Returns:
      An AdbResponse
    """

    request = request.settings
    response = adb_pb2.AdbResponse()
    # Every SettingsRequest should have a namespace.
    if (
        request.name_space
        == adb_pb2.AdbRequest.SettingsRequest.Namespace.UNKNOWN
    ):
      response.status = adb_pb2.AdbResponse.Status.FAILED_PRECONDITION
      response.error_message = (
          f'Unknown SettingsRequest.name_space. Got: {request}.')
      return response

    namespace = adb_pb2.AdbRequest.SettingsRequest.Namespace.Name(
        request.name_space).lower()

    match request.WhichOneof('verb'):
      case 'get':
        get = request.get
        if not get.key:
          response.status = adb_pb2.AdbResponse.Status.FAILED_PRECONDITION
          response.error_message = (
              f'Empty SettingsRequest.get.key. Got: {request}.'
          )
          return response
        response, command_output = self._execute_command(
            ['shell', 'settings', 'get', namespace, get.key], timeout=timeout
        )
        response.settings.output = command_output
      case 'put':
        put = request.put
        if not put.key
Download .txt
gitextract_70x6n_qo/

├── .github/
│   └── workflows/
│       └── tests.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── android_env/
│   ├── __init__.py
│   ├── apps/
│   │   ├── MODULE.bazel
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── google/
│   │   │           └── androidenv/
│   │   │               ├── accessibilityforwarder/
│   │   │               │   ├── AccessibilityForwarder.kt
│   │   │               │   ├── AccessibilityForwarderTest.kt
│   │   │               │   ├── AccessibilityTreeCreator.kt
│   │   │               │   ├── AccessibilityTreeCreatorTest.kt
│   │   │               │   ├── AndroidManifest.xml
│   │   │               │   ├── AndroidManifest_lite.xml
│   │   │               │   ├── FlagsBroadcastReceiver.kt
│   │   │               │   ├── FlagsBroadcastReceiverTest.kt
│   │   │               │   ├── LogFlags.kt
│   │   │               │   ├── ParentChildNodePair.kt
│   │   │               │   ├── UniqueIdsGenerator.kt
│   │   │               │   └── res/
│   │   │               │       └── xml/
│   │   │               │           └── accessibility_forwarder_service.xml
│   │   │               └── catch/
│   │   │                   ├── AndroidManifest.xml
│   │   │                   ├── BUILD.bazel
│   │   │                   ├── GameLogic.kt
│   │   │                   ├── GameLogicThread.kt
│   │   │                   ├── MainActivity.kt
│   │   │                   ├── RenderThread.kt
│   │   │                   ├── res/
│   │   │                   │   ├── layout/
│   │   │                   │   │   └── main.xml
│   │   │                   │   └── values/
│   │   │                   │       └── strings.xml
│   │   │                   └── sprite/
│   │   │                       ├── BUILD.bazel
│   │   │                       ├── Background.kt
│   │   │                       ├── Ball.kt
│   │   │                       ├── LineSegment.kt
│   │   │                       ├── Paddle.kt
│   │   │                       ├── Point.kt
│   │   │                       └── Sprite.kt
│   │   └── javatests/
│   │       └── com/
│   │           └── google/
│   │               └── androidenv/
│   │                   └── catch/
│   │                       ├── AndroidManifest.xml
│   │                       ├── BUILD.bazel
│   │                       ├── GameLogicTest.kt
│   │                       ├── GameLogicThreadTest.kt
│   │                       ├── MainActivityTest.kt
│   │                       ├── RenderThreadTest.kt
│   │                       └── sprite/
│   │                           ├── BUILD.bazel
│   │                           ├── BackgroundTest.kt
│   │                           ├── BallTest.kt
│   │                           ├── PaddleTest.kt
│   │                           └── SpriteTest.kt
│   ├── components/
│   │   ├── __init__.py
│   │   ├── action_fns.py
│   │   ├── action_fns_test.py
│   │   ├── action_type.py
│   │   ├── adb_call_parser.py
│   │   ├── adb_call_parser_test.py
│   │   ├── adb_controller.py
│   │   ├── adb_controller_test.py
│   │   ├── adb_log_stream.py
│   │   ├── adb_log_stream_test.py
│   │   ├── app_screen_checker.py
│   │   ├── app_screen_checker_test.py
│   │   ├── config_classes.py
│   │   ├── coordinator.py
│   │   ├── coordinator_test.py
│   │   ├── device_settings.py
│   │   ├── device_settings_test.py
│   │   ├── dumpsys_thread.py
│   │   ├── dumpsys_thread_test.py
│   │   ├── errors.py
│   │   ├── errors_test.py
│   │   ├── log_stream.py
│   │   ├── log_stream_test.py
│   │   ├── logcat_thread.py
│   │   ├── logcat_thread_test.py
│   │   ├── pixel_fns.py
│   │   ├── pixel_fns_test.py
│   │   ├── setup_step_interpreter.py
│   │   ├── setup_step_interpreter_test.py
│   │   ├── simulators/
│   │   │   ├── __init__.py
│   │   │   ├── base_simulator.py
│   │   │   ├── base_simulator_test.py
│   │   │   ├── emulator/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── emulator_launcher.py
│   │   │   │   ├── emulator_launcher_test.py
│   │   │   │   ├── emulator_simulator.py
│   │   │   │   └── emulator_simulator_test.py
│   │   │   └── fake/
│   │   │       ├── __init__.py
│   │   │       ├── fake_simulator.py
│   │   │       └── fake_simulator_test.py
│   │   ├── specs.py
│   │   ├── specs_test.py
│   │   ├── task_manager.py
│   │   └── task_manager_test.py
│   ├── env_interface.py
│   ├── environment.py
│   ├── environment_test.py
│   ├── loader.py
│   ├── loader_test.py
│   ├── proto/
│   │   ├── __init__.py
│   │   ├── a11y/
│   │   │   ├── __init__.py
│   │   │   ├── a11y.proto
│   │   │   ├── android_accessibility_action.proto
│   │   │   ├── android_accessibility_forest.proto
│   │   │   ├── android_accessibility_node_info.proto
│   │   │   ├── android_accessibility_node_info_clickable_span.proto
│   │   │   ├── android_accessibility_tree.proto
│   │   │   ├── android_accessibility_window_info.proto
│   │   │   └── rect.proto
│   │   ├── adb.proto
│   │   ├── emulator_controller.proto
│   │   ├── snapshot.proto
│   │   ├── snapshot_service.proto
│   │   ├── state.proto
│   │   └── task.proto
│   └── wrappers/
│       ├── __init__.py
│       ├── a11y/
│       │   ├── __init__.py
│       │   ├── a11y_events.py
│       │   ├── a11y_events_test.py
│       │   ├── a11y_forests.py
│       │   ├── a11y_forests_test.py
│       │   ├── a11y_servicer.py
│       │   └── a11y_servicer_test.py
│       ├── a11y_grpc_wrapper.py
│       ├── a11y_grpc_wrapper_test.py
│       ├── base_wrapper.py
│       ├── base_wrapper_test.py
│       ├── discrete_action_wrapper.py
│       ├── discrete_action_wrapper_test.py
│       ├── flat_interface_wrapper.py
│       ├── flat_interface_wrapper_test.py
│       ├── float_pixels_wrapper.py
│       ├── float_pixels_wrapper_test.py
│       ├── gym_wrapper.py
│       ├── gym_wrapper_test.py
│       ├── image_rescale_wrapper.py
│       ├── image_rescale_wrapper_test.py
│       ├── last_action_wrapper.py
│       ├── last_action_wrapper_test.py
│       ├── rate_limit_wrapper.py
│       ├── rate_limit_wrapper_test.py
│       ├── tap_action_wrapper.py
│       └── tap_action_wrapper_test.py
├── docs/
│   ├── emulator_guide.md
│   ├── environment.md
│   ├── example_tasks.md
│   ├── instructions.md
│   └── tasks_guide.md
├── examples/
│   ├── __init__.py
│   ├── run_acme_agent.py
│   ├── run_human_agent.py
│   └── run_random_agent.py
├── pyproject.toml
└── setup.py
Download .txt
SYMBOL INDEX (809 symbols across 75 files)

FILE: android_env/components/action_fns.py
  function send_action_to_simulator (line 26) | def send_action_to_simulator(
  function _prepare_touch_action (line 69) | def _prepare_touch_action(
  function _split_touch_action (line 101) | def _split_touch_action(
  function lift_all_fingers_action (line 118) | def lift_all_fingers_action(num_fingers: int) -> dict[str, np.ndarray]:

FILE: android_env/components/action_fns_test.py
  class ActionFnsTest (line 27) | class ActionFnsTest(parameterized.TestCase):
    method test_send_action_to_simulator_missing_action_type (line 29) | def test_send_action_to_simulator_missing_action_type(self):
    method test_send_action_to_simulator_sendactionerror (line 47) | def test_send_action_to_simulator_sendactionerror(self):
    method test_send_action_to_simulator_touch_success_one_finger (line 71) | def test_send_action_to_simulator_touch_success_one_finger(self):
    method test_send_action_to_simulator_touch_success_multiple_finger (line 96) | def test_send_action_to_simulator_touch_success_multiple_finger(self):
    method test_send_action_to_simulator_keydown_success (line 127) | def test_send_action_to_simulator_keydown_success(self):
    method test_send_action_to_simulator_keyup_success (line 150) | def test_send_action_to_simulator_keyup_success(self):
    method test_send_action_to_simulator_keypress_success (line 173) | def test_send_action_to_simulator_keypress_success(self):
    method test_lift_all_fingers_action (line 216) | def test_lift_all_fingers_action(

FILE: android_env/components/action_type.py
  class ActionType (line 30) | class ActionType(enum.IntEnum):

FILE: android_env/components/adb_call_parser.py
  class AdbCallParser (line 40) | class AdbCallParser:
    method __init__ (line 43) | def __init__(self, adb_controller: adb_control.AdbController):
    method _execute_command (line 65) | def _execute_command(
    method parse (line 93) | def parse(self, request: adb_pb2.AdbRequest) -> adb_pb2.AdbResponse:
    method _force_stop (line 113) | def _force_stop(
    method _fetch_current_task_id (line 138) | def _fetch_current_task_id(
    method _start_screen_pinning (line 185) | def _start_screen_pinning(
    method _send_broadcast (line 219) | def _send_broadcast(
    method _install_apk (line 252) | def _install_apk(
    method _start_activity (line 307) | def _start_activity(
    method _press_button (line 355) | def _press_button(
    method _handle_uninstall_package (line 382) | def _handle_uninstall_package(
    method _get_current_activity (line 421) | def _get_current_activity(
    method _get_orientation (line 475) | def _get_orientation(
    method _push (line 536) | def _push(
    method _pull (line 567) | def _pull(
    method _input_text (line 600) | def _input_text(
    method _tap (line 623) | def _tap(
    method _handle_settings (line 653) | def _handle_settings(
    method _handle_generic (line 756) | def _handle_generic(
    method _handle_package_manager (line 775) | def _handle_package_manager(
    method _handle_dumpsys (line 847) | def _handle_dumpsys(

FILE: android_env/components/adb_call_parser_test.py
  class AdbCallParserTest (line 30) | class AdbCallParserTest(parameterized.TestCase):
    method test_unknown_command (line 32) | def test_unknown_command(self):
    method test_invalid_timeout (line 42) | def test_invalid_timeout(self):
    method test_install_apk_file_not_found (line 55) | def test_install_apk_file_not_found(self, mock_exists):
    method test_install_apk_successful (line 69) | def test_install_apk_successful(self, mock_exists):
    method test_install_apk_from_blob (line 84) | def test_install_apk_from_blob(self, mock_tempfile):
    method test_start_activity_empty_full_activity (line 114) | def test_start_activity_empty_full_activity(self):
    method test_start_activity_successful (line 125) | def test_start_activity_successful(self):
    method test_start_activity_successful_no_force_stop (line 146) | def test_start_activity_successful_no_force_stop(self):
    method test_start_activity_error (line 167) | def test_start_activity_error(self):
    method test_force_stop (line 183) | def test_force_stop(self):
    method test_grant_permissions_empty_package_name (line 194) | def test_grant_permissions_empty_package_name(self):
    method test_grant_permissions_empty_permissions (line 204) | def test_grant_permissions_empty_permissions(self):
    method test_grant_permissions_successful (line 214) | def test_grant_permissions_successful(self):
    method test_press_button_invalid_button (line 229) | def test_press_button_invalid_button(self):
    method test_press_button_successful (line 239) | def test_press_button_successful(self):
    method test_start_screen_pinning_package_not_found (line 268) | def test_start_screen_pinning_package_not_found(self):
    method test_start_screen_pinning_successful (line 282) | def test_start_screen_pinning_successful(self):
    method test_start_screen_pinning_base_activity (line 298) | def test_start_screen_pinning_base_activity(self):
    method test_start_screen_pinning_top_activity (line 314) | def test_start_screen_pinning_top_activity(self):
    method test_send_broadcast_empty_action (line 330) | def test_send_broadcast_empty_action(self):
    method test_send_broadcast_successful (line 340) | def test_send_broadcast_successful(self):
    method test_send_broadcast_with_component_successful (line 349) | def test_send_broadcast_with_component_successful(self):
    method test_uninstall_package_empty_package_name (line 359) | def test_uninstall_package_empty_package_name(self):
    method test_uninstall_package_successful (line 369) | def test_uninstall_package_successful(self):
    method test_get_current_activity_no_visible_task (line 379) | def test_get_current_activity_no_visible_task(self):
    method test_get_orientation_empty_dumpsys (line 395) | def test_get_orientation_empty_dumpsys(self):
    method test_get_orientation_invalid_device_no_surface_orientation (line 407) | def test_get_orientation_invalid_device_no_surface_orientation(self):
    method test_get_orientation_success (line 429) | def test_get_orientation_success(
    method test_get_current_activity_no_matches (line 449) | def test_get_current_activity_no_matches(self):
    method test_get_current_activity_successful (line 470) | def test_get_current_activity_successful(self):
    method test_push_no_path (line 489) | def test_push_no_path(self):
    method test_push_successful (line 501) | def test_push_successful(self):
    method test_pull_no_path (line 523) | def test_pull_no_path(self):
    method test_pull_successful (line 535) | def test_pull_successful(self, mock_open):
    method test_input_text_no_text (line 559) | def test_input_text_no_text(self):
    method test_input_text_successful (line 570) | def test_input_text_successful(self):
    method test_tap_failed (line 591) | def test_tap_failed(self, request: adb_pb2.AdbRequest):
    method test_tap_successful (line 601) | def test_tap_successful(self):
    method test_settings_failures (line 638) | def test_settings_failures(self, request):
    method test_settings_success_get (line 649) | def test_settings_success_get(self):
    method test_settings_success_put (line 666) | def test_settings_success_put(self):
    method test_settings_success_delete (line 683) | def test_settings_success_delete(self):
    method test_settings_success_reset (line 724) | def test_settings_success_reset(self, mode, package_name, expected_arg):
    method test_settings_success_list (line 742) | def test_settings_success_list(self):
    method test_generic_command (line 760) | def test_generic_command(self):
    method test_generic_command_adb_error (line 776) | def test_generic_command_adb_error(self):
    method test_generic_command_timeout (line 792) | def test_generic_command_timeout(self):
    method test_package_manager_list_bad_output (line 828) | def test_package_manager_list_bad_output(self, request):
    method test_package_manager_list_features (line 839) | def test_package_manager_list_features(self):
    method test_package_manager_list_libraries (line 873) | def test_package_manager_list_libraries(self):
    method test_package_manager_list_packages (line 927) | def test_package_manager_list_packages(self):
    method test_package_manager_clear_no_package_name (line 953) | def test_package_manager_clear_no_package_name(self):
    method test_package_manager_clear_successful_no_user_id (line 970) | def test_package_manager_clear_successful_no_user_id(self):
    method test_package_manager_clear_successful_with_user_id (line 988) | def test_package_manager_clear_successful_with_user_id(self):
    method test_dumpsys_empty_request (line 1006) | def test_dumpsys_empty_request(self):
    method test_dumpsys_negative_timeouts (line 1028) | def test_dumpsys_negative_timeouts(self, request):
    method test_dumpsys_timeout_successful (line 1046) | def test_dumpsys_timeout_successful(self, timeout_sec, timeout_ms, exp...
    method test_dumpsys_priority_timeout_successful (line 1073) | def test_dumpsys_priority_timeout_successful(self, priority, expected):
    method test_dumpsys_list_only_cannot_be_combined (line 1104) | def test_dumpsys_list_only_cannot_be_combined(
    method test_dumpsys_list_only_success (line 1125) | def test_dumpsys_list_only_success(self):
    method test_dumpsys_skip_services_cannot_combine_with_service (line 1139) | def test_dumpsys_skip_services_cannot_combine_with_service(self):
    method test_dumpsys_skip_services (line 1155) | def test_dumpsys_skip_services(self):
    method test_dumpsys_single_service (line 1170) | def test_dumpsys_single_service(self):
    method test_dumpsys_single_service_with_args (line 1184) | def test_dumpsys_single_service_with_args(self):
    method test_dumpsys_single_service_with_proto (line 1199) | def test_dumpsys_single_service_with_proto(self):

FILE: android_env/components/adb_controller.py
  class AdbController (line 27) | class AdbController:
    method __init__ (line 30) | def __init__(self, config: config_classes.AdbControllerConfig):
    method command_prefix (line 53) | def command_prefix(self, include_device_name: bool = True) -> list[str]:
    method init_server (line 71) | def init_server(self, timeout: float | None = None):
    method _restart_server (line 85) | def _restart_server(self, timeout: float | None = None):
    method execute_command (line 105) | def execute_command(

FILE: android_env/components/adb_controller_test.py
  class AdbControllerTest (line 31) | class AdbControllerTest(absltest.TestCase):
    method setUp (line 33) | def setUp(self):
    method tearDown (line 40) | def tearDown(self):
    method test_init_server (line 49) | def test_init_server(self, mock_sleep, mock_check_output):
    method test_init_server_with_adb_server_port_from_os_env (line 77) | def test_init_server_with_adb_server_port_from_os_env(
    method test_restart_server (line 114) | def test_restart_server(self, mock_sleep, mock_check_output):
    method test_invalid_command (line 173) | def test_invalid_command(self, mock_sleep, mock_check_output):
    method test_avoid_infinite_recursion (line 242) | def test_avoid_infinite_recursion(self, mock_sleep, mock_check_output):
  class AdbControllerInitTest (line 264) | class AdbControllerInitTest(absltest.TestCase):
    method test_deletes_problem_env_vars (line 266) | def test_deletes_problem_env_vars(self):
    method test_use_adb_server_port_from_os_env_retains_os_env_vars (line 280) | def test_use_adb_server_port_from_os_env_retains_os_env_vars(self):

FILE: android_env/components/adb_log_stream.py
  class AdbLogStream (line 27) | class AdbLogStream(log_stream.LogStream):
    method __init__ (line 30) | def __init__(self, adb_command_prefix: list[str], verbose: bool = False):
    method _get_stream_output (line 34) | def _get_stream_output(self):
    method stop_stream (line 52) | def stop_stream(self):

FILE: android_env/components/adb_log_stream_test.py
  class FakeAdbSubprocess (line 25) | class FakeAdbSubprocess:
    method stdout (line 28) | def stdout(self):
    method kill (line 31) | def kill(self):
  class AdbLogStreamTest (line 35) | class AdbLogStreamTest(absltest.TestCase):
    method test_get_stream_output (line 39) | def test_get_stream_output(self, mock_popen, unused_mock_check_output):
    method test_stop_stream_before_get_stream_output (line 54) | def test_stop_stream_before_get_stream_output(self):

FILE: android_env/components/app_screen_checker.py
  class _DumpsysNode (line 31) | class _DumpsysNode:
    method __init__ (line 34) | def __init__(self, data: str):
    method data (line 39) | def data(self) -> str:
    method children (line 43) | def children(self) -> list[Self]:
    method find_child (line 46) | def find_child(
    method __repr__ (line 75) | def __repr__(self):
    method print_tree (line 78) | def print_tree(self, indent: int = 2):
  function build_tree_from_dumpsys_output (line 85) | def build_tree_from_dumpsys_output(dumpsys_output: str) -> _DumpsysNode:
  function matches_path (line 129) | def matches_path(
  class AppScreenChecker (line 176) | class AppScreenChecker:
    class Outcome (line 179) | class Outcome(enum.IntEnum):
    method __init__ (line 192) | def __init__(self, adb_call_parser: adb_call_parser_lib.AdbCallParser,
    method matches_current_app_screen (line 202) | def matches_current_app_screen(self) -> enum.IntEnum:
    method wait_for_app_screen (line 241) | def wait_for_app_screen(self, timeout_sec: float) -> float:

FILE: android_env/components/app_screen_checker_test.py
  function _flatten_tree (line 29) | def _flatten_tree(
  class AppScreenCheckerTest (line 38) | class AppScreenCheckerTest(absltest.TestCase):
    method test_build_tree_from_dumpsys_output (line 42) | def test_build_tree_from_dumpsys_output(self):
    method test_build_forest_from_dumpsys_output (line 96) | def test_build_forest_from_dumpsys_output(self):
    method test_no_view_hierarchy_matches_path (line 140) | def test_no_view_hierarchy_matches_path(self):
    method test_matches_path (line 160) | def test_matches_path(self):
    method test_matches_path_one_level_deep (line 195) | def test_matches_path_one_level_deep(self):
    method test_wait_for_app_screen_zero_timeout (line 232) | def test_wait_for_app_screen_zero_timeout(self):
    method test_wait_for_app_screen_successful (line 246) | def test_wait_for_app_screen_successful(self):

FILE: android_env/components/config_classes.py
  class AdbControllerConfig (line 23) | class AdbControllerConfig:
  class DeviceSettingsConfig (line 47) | class DeviceSettingsConfig:
  class CoordinatorConfig (line 61) | class CoordinatorConfig:
  class SimulatorConfig (line 79) | class SimulatorConfig:
  class GPUMode (line 91) | class GPUMode(enum.Enum):
  class EmulatorLauncherConfig (line 100) | class EmulatorLauncherConfig:
  class EmulatorConfig (line 144) | class EmulatorConfig(SimulatorConfig):
  class FakeSimulatorConfig (line 167) | class FakeSimulatorConfig(SimulatorConfig):
  class TaskManagerConfig (line 175) | class TaskManagerConfig:
  class TaskConfig (line 193) | class TaskConfig:
  class FilesystemTaskConfig (line 201) | class FilesystemTaskConfig(TaskConfig):
  class AndroidEnvConfig (line 209) | class AndroidEnvConfig:

FILE: android_env/components/coordinator.py
  class Coordinator (line 36) | class Coordinator:
    method __init__ (line 39) | def __init__(
    method action_spec (line 81) | def action_spec(self) -> dict[str, dm_env.specs.Array]:
    method observation_spec (line 87) | def observation_spec(self) -> dict[str, dm_env.specs.Array]:
    method _should_periodic_relaunch (line 93) | def _should_periodic_relaunch(self) -> bool:
    method _launch_simulator (line 114) | def _launch_simulator(self, max_retries: int = 3):
    method _create_adb_call_parser (line 171) | def _create_adb_call_parser(self):
    method execute_adb_call (line 177) | def execute_adb_call(self, call: adb_pb2.AdbRequest) -> adb_pb2.AdbRes...
    method rl_reset (line 180) | def rl_reset(self) -> dm_env.TimeStep:
    method rl_step (line 213) | def rl_step(self, agent_action: dict[str, np.ndarray]) -> dm_env.TimeS...
    method _gather_simulator_signals (line 248) | def _gather_simulator_signals(self) -> dict[str, np.ndarray]:
    method __del__ (line 266) | def __del__(self):
    method stats (line 269) | def stats(self) -> dict[str, Any]:
    method close (line 274) | def close(self):

FILE: android_env/components/coordinator_test.py
  class CoordinatorTest (line 39) | class CoordinatorTest(parameterized.TestCase):
    method setUp (line 41) | def setUp(self):
    method tearDown (line 63) | def tearDown(self):
    method test_relaunch_simulator (line 68) | def test_relaunch_simulator(self, unused_mock_sleep):
    method test_reset (line 75) | def test_reset(self, unused_mock_sleep):
    method test_reset_error_sending_action (line 84) | def test_reset_error_sending_action(self, unused_mock_sleep):
    method test_lift_all_fingers (line 94) | def test_lift_all_fingers(self, unused_mock_sleep):
    method test_process_action (line 113) | def test_process_action(self, unused_mock_sleep):
    method test_process_action_error (line 142) | def test_process_action_error(self, unused_mock_sleep):
    method test_execute_action_touch (line 160) | def test_execute_action_touch(self, unused_mock_sleep):
    method test_execute_multitouch_action (line 186) | def test_execute_multitouch_action(self, unused_mock_sleep):
    method test_execute_action_repeat (line 224) | def test_execute_action_repeat(self, unused_mock_sleep):
    method test_execute_action_error (line 245) | def test_execute_action_error(self, unused_mock_sleep):
    method test_max_restarts_setup_steps (line 259) | def test_max_restarts_setup_steps(self, unused_mock_sleep):
    method test_execute_adb_call (line 269) | def test_execute_adb_call(self, unused_mock_sleep):

FILE: android_env/components/device_settings.py
  class DeviceSettings (line 41) | class DeviceSettings:
    method __init__ (line 44) | def __init__(self, simulator: base_simulator.BaseSimulator):
    method update (line 54) | def update(self, config: config_classes.DeviceSettingsConfig) -> None:
    method screen_width (line 69) | def screen_width(self) -> int:
    method screen_height (line 74) | def screen_height(self) -> int:
    method get_orientation (line 79) | def get_orientation(self) -> np.ndarray:
    method _update_screen_size (line 90) | def _update_screen_size(self) -> None:
    method _set_show_touches (line 97) | def _set_show_touches(self, show: bool) -> None:
    method _set_show_pointer_location (line 111) | def _set_show_pointer_location(self, show: bool) -> None:
    method _set_status_navigation_bars (line 125) | def _set_status_navigation_bars(
    method _update_orientation (line 150) | def _update_orientation(self) -> None:

FILE: android_env/components/device_settings_test.py
  class DeviceSettingsTest (line 26) | class DeviceSettingsTest(parameterized.TestCase):
    method test_screen_size_before_update (line 28) | def test_screen_size_before_update(self):
    method test_screen_size_after_update (line 43) | def test_screen_size_after_update(self):
    method test_update (line 159) | def test_update(
    method test_get_orientation_bad_response (line 178) | def test_get_orientation_bad_response(self):
    method test_get_orientation_bad_orientation (line 193) | def test_get_orientation_bad_orientation(self):
    method test_get_orientation_success (line 208) | def test_get_orientation_success(self):

FILE: android_env/components/dumpsys_thread.py
  class DumpsysThread (line 26) | class DumpsysThread:
    method __init__ (line 29) | def __init__(
    method check_user_exited (line 60) | def check_user_exited(self, timeout: float | None = None) -> bool:
    method _check_impl (line 94) | def _check_impl(self) -> bool:

FILE: android_env/components/dumpsys_thread_test.py
  class DumpsysThreadTest (line 25) | class DumpsysThreadTest(absltest.TestCase):
    method setUp (line 27) | def setUp(self):
    method test_unexpected_activity (line 32) | def test_unexpected_activity(self):
    method test_unexpected_view_hierarchy (line 43) | def test_unexpected_view_hierarchy(self):
    method test_success (line 51) | def test_success(self):
    method test_skipped (line 59) | def test_skipped(self):

FILE: android_env/components/errors.py
  class AndroidEnvError (line 19) | class AndroidEnvError(Exception):
  class ReadObservationError (line 27) | class ReadObservationError(AndroidEnvError):
  class CoordinatorError (line 33) | class CoordinatorError(AndroidEnvError):
  class TooManyRestartsError (line 39) | class TooManyRestartsError(CoordinatorError):
  class AdbControllerError (line 45) | class AdbControllerError(AndroidEnvError):
  class SimulatorError (line 51) | class SimulatorError(AndroidEnvError):
  class SendActionError (line 57) | class SendActionError(AndroidEnvError):
  class StepCommandError (line 63) | class StepCommandError(AndroidEnvError):
  class WaitForAppScreenError (line 69) | class WaitForAppScreenError(StepCommandError):
  class CheckInstallError (line 75) | class CheckInstallError(StepCommandError):
  function from_code (line 81) | def from_code(code: int, msg: str = '') -> AndroidEnvError | None:

FILE: android_env/components/errors_test.py
  class ErrorsTest (line 23) | class ErrorsTest(parameterized.TestCase):
    method test_error_codes (line 36) | def test_error_codes(self, error, expected_error_code):
    method test_error_codes_unique (line 41) | def test_error_codes_unique(self):
    method test_all_errors_are_androidenv_errors (line 69) | def test_all_errors_are_androidenv_errors(self, error):
    method test_from_code_unsupported_code (line 79) | def test_from_code_unsupported_code(self, code: int):
    method test_from_code (line 98) | def test_from_code(self, code: int, expected_class: errors.AndroidEnvE...

FILE: android_env/components/log_stream.py
  class LogStream (line 24) | class LogStream(metaclass=abc.ABCMeta):
    method __init__ (line 27) | def __init__(self, verbose: bool = False):
    method get_stream_output (line 32) | def get_stream_output(self) -> Generator[str, None, None]:
    method _get_stream_output (line 41) | def _get_stream_output(self):
    method stop_stream (line 46) | def stop_stream(self) -> None:
    method pause_stream (line 52) | def pause_stream(self) -> None:
    method resume_stream (line 57) | def resume_stream(self) -> None:
    method set_log_filters (line 62) | def set_log_filters(self, log_filters: Sequence[str]):

FILE: android_env/components/log_stream_test.py
  class FakeLogStream (line 22) | class FakeLogStream(log_stream.LogStream):
    method __init__ (line 24) | def __init__(self, filter_name: str):
    method _get_stream_output (line 28) | def _get_stream_output(self):
    method stop_stream (line 45) | def stop_stream(self):
  class LogStreamTest (line 50) | class LogStreamTest(absltest.TestCase):
    method test_get_stream_output (line 52) | def test_get_stream_output(self):
    method test_set_log_filters (line 68) | def test_set_log_filters(self):
    method test_pause_resume_stream (line 82) | def test_pause_resume_stream(self):

FILE: android_env/components/logcat_thread.py
  class EventListener (line 28) | class EventListener:
  class LogcatThread (line 35) | class LogcatThread:
    method __init__ (line 38) | def __init__(self, log_stream: log_stream_lib.LogStream):
    method add_event_listener (line 57) | def add_event_listener(self, event_listener: EventListener) -> None:
    method remove_event_listener (line 64) | def remove_event_listener(self, event_listener: EventListener) -> None:
    method line_ready (line 72) | def line_ready(self) -> threading.Event:
    method pause (line 76) | def pause(self):
    method resume (line 79) | def resume(self):
    method kill (line 88) | def kill(self):
    method _process_logs (line 93) | def _process_logs(self) -> None:

FILE: android_env/components/logcat_thread_test.py
  class FakeStream (line 27) | class FakeStream:
    method __init__ (line 30) | def __init__(self):
    method send_value (line 35) | def send_value(self, value):
    method has_next_value (line 39) | def has_next_value(self):
    method kill (line 42) | def kill(self):
    method reset (line 45) | def reset(self):
    method __iter__ (line 48) | def __iter__(self):
  function make_stdout (line 60) | def make_stdout(data):
  class FakeLogStream (line 65) | class FakeLogStream(log_stream.LogStream):
    method __init__ (line 68) | def __init__(self):
    method _get_stream_output (line 73) | def _get_stream_output(self):
    method stop_stream (line 76) | def stop_stream(self):
    method reset (line 80) | def reset(self):
  class LogcatThreadTest (line 85) | class LogcatThreadTest(absltest.TestCase):
    method setUp (line 87) | def setUp(self):
    method tearDown (line 91) | def tearDown(self):
    method test_set_filters (line 95) | def test_set_filters(self):
    method test_kill (line 102) | def test_kill(self):
    method test_listeners (line 108) | def test_listeners(self):
    method test_resume_does_not_recreate_alive_thread (line 145) | def test_resume_does_not_recreate_alive_thread(self):
    method test_resume_recreates_thread (line 154) | def test_resume_recreates_thread(self):

FILE: android_env/components/pixel_fns.py
  function touch_position_to_pixel_position (line 24) | def touch_position_to_pixel_position(
  function transpose_pixels (line 34) | def transpose_pixels(frame: np.ndarray) -> np.ndarray:
  function orient_pixels (line 39) | def orient_pixels(frame: np.ndarray, orientation: int) -> np.ndarray:
  function convert_int_to_float (line 57) | def convert_int_to_float(data: np.ndarray, data_spec: specs.Array):

FILE: android_env/components/pixel_fns_test.py
  class UtilsTest (line 25) | class UtilsTest(parameterized.TestCase):
    method test_touch_position_to_pixel_position (line 33) | def test_touch_position_to_pixel_position(
    method test_transpose_pixels (line 42) | def test_transpose_pixels(self):
    method test_orient_pixels (line 48) | def test_orient_pixels(self):
    method test_convert_int_to_float_bounded_array (line 74) | def test_convert_int_to_float_bounded_array(self):
    method test_convert_int_to_float_bounded_array_broadcast (line 87) | def test_convert_int_to_float_bounded_array_broadcast(self):
    method test_convert_int_to_float_no_bounds (line 95) | def test_convert_int_to_float_no_bounds(self):

FILE: android_env/components/setup_step_interpreter.py
  class SetupStepInterpreter (line 31) | class SetupStepInterpreter:
    method __init__ (line 34) | def __init__(self, adb_call_parser: adb_call_parser_lib.AdbCallParser):
    method stats (line 49) | def stats(self) -> dict[str, Any]:
    method interpret (line 52) | def interpret(self, setup_steps: Sequence[task_pb2.SetupStep]) -> None:
    method _process_step_command (line 60) | def _process_step_command(self, step_cmd: task_pb2.SetupStep) -> None:
    method _execute_step_cmd (line 113) | def _execute_step_cmd(
    method _check_success (line 136) | def _check_success(
    method _check_install (line 161) | def _check_install(self, check_install: task_pb2.CheckInstall) -> None:

FILE: android_env/components/setup_step_interpreter_test.py
  function _to_proto (line 30) | def _to_proto(proto_class, text):
  class SetupStepInterpreterTest (line 36) | class SetupStepInterpreterTest(absltest.TestCase):
    method setUp (line 38) | def setUp(self):
    method test_empty_setup_steps (line 43) | def test_empty_setup_steps(self):
    method test_none_setup_steps (line 52) | def test_none_setup_steps(self):
    method test_invalid_setup_step (line 62) | def test_invalid_setup_step(self):
    method test_adb_install_apk_filesystem (line 69) | def test_adb_install_apk_filesystem(self):
    method test_adb_force_stop (line 91) | def test_adb_force_stop(self):
    method test_adb_start_activity (line 106) | def test_adb_start_activity(self):
    method test_adb_single_tap (line 127) | def test_adb_single_tap(self):
    method test_adb_press_button (line 144) | def test_adb_press_button(self):
    method test_adb_start_screen_pinning (line 168) | def test_adb_start_screen_pinning(self):
    method test_time_sleep (line 188) | def test_time_sleep(self, mock_sleep):
    method test_wait_for_app_screen_empty_activity (line 197) | def test_wait_for_app_screen_empty_activity(self, unused_mock_sleep):
    method test_check_install_not_installed (line 207) | def test_check_install_not_installed(self, unused_mock_sleep):
    method test_check_install_installed (line 229) | def test_check_install_installed(self):
    method test_num_retries_failure (line 250) | def test_num_retries_failure(self):
    method test_num_retries_success (line 275) | def test_num_retries_success(self, unused_mock_sleep):
    method test_retry_step (line 311) | def test_retry_step(self):

FILE: android_env/components/simulators/base_simulator.py
  class BaseSimulator (line 32) | class BaseSimulator(metaclass=abc.ABCMeta):
    method __init__ (line 35) | def __init__(self, config: config_classes.SimulatorConfig):
    method get_logs (line 52) | def get_logs(self) -> str:
    method adb_device_name (line 57) | def adb_device_name(self) -> str:
    method create_adb_controller (line 61) | def create_adb_controller(self) -> adb_controller.AdbController:
    method create_log_stream (line 65) | def create_log_stream(self) -> log_stream.LogStream:
    method launch (line 68) | def launch(self) -> None:
    method _launch_impl (line 95) | def _launch_impl(self) -> None:
    method send_touch (line 99) | def send_touch(self, touches: list[tuple[int, int, bool, int]]) -> None:
    method send_key (line 112) | def send_key(self, keycode: np.int32, event_type: str) -> None:
    method load_state (line 121) | def load_state(
    method save_state (line 136) | def save_state(
    method get_screenshot (line 151) | def get_screenshot(self) -> np.ndarray:
    method _get_screenshot_impl (line 161) | def _get_screenshot_impl(self) -> np.ndarray:
    method close (line 169) | def close(self):
  class InteractionThread (line 177) | class InteractionThread(threading.Thread):
    method __init__ (line 180) | def __init__(
    method run (line 191) | def run(self):
    method stop (line 203) | def stop(self):
    method screenshot (line 207) | def screenshot(self) -> np.ndarray:

FILE: android_env/components/simulators/base_simulator_test.py
  class BaseSimulatorTest (line 30) | class BaseSimulatorTest(absltest.TestCase):
    method test_launch (line 32) | def test_launch(self):
    method test_launch_close (line 39) | def test_launch_close(self):
    method test_get_screenshot (line 48) | def test_get_screenshot(self):
    method test_print_logs_on_exception (line 58) | def test_print_logs_on_exception(self):
    method test_get_screenshot_error_async (line 71) | def test_get_screenshot_error_async(self):
    method test_get_screenshot_faster_than_screenshot_impl (line 98) | def test_get_screenshot_faster_than_screenshot_impl(self):
    method test_get_screenshot_slower_than_screenshot_impl (line 125) | def test_get_screenshot_slower_than_screenshot_impl(self):
    method test_interaction_thread_closes_upon_relaunch (line 153) | def test_interaction_thread_closes_upon_relaunch(self):

FILE: android_env/components/simulators/emulator/emulator_launcher.py
  class EmulatorLauncher (line 27) | class EmulatorLauncher:
    method __init__ (line 30) | def __init__(
    method logfile_path (line 54) | def logfile_path(self) -> str:
    method launch_emulator_process (line 57) | def launch_emulator_process(self) -> None:
    method confirm_shutdown (line 145) | def confirm_shutdown(self) -> None:
    method close (line 161) | def close(self):
    method __del__ (line 168) | def __del__(self):

FILE: android_env/components/simulators/emulator/emulator_launcher_test.py
  class EmulatorLauncherTest (line 30) | class EmulatorLauncherTest(parameterized.TestCase):
    method setUp (line 32) | def setUp(self):
    method test_launch (line 84) | def test_launch(
    method test_grpc_port (line 134) | def test_grpc_port(
    method test_snapshot (line 185) | def test_snapshot(
    method test_network_restrict (line 241) | def test_network_restrict(

FILE: android_env/components/simulators/emulator/emulator_simulator.py
  function _is_existing_emulator_provided (line 45) | def _is_existing_emulator_provided(
  function _pick_adb_port (line 57) | def _pick_adb_port() -> int:
  function _pick_emulator_grpc_port (line 73) | def _pick_emulator_grpc_port() -> int:
  class EmulatorBootError (line 89) | class EmulatorBootError(errors.SimulatorError):
  class EmulatorCrashError (line 93) | class EmulatorCrashError(errors.SimulatorError):
  class EmulatorSimulator (line 97) | class EmulatorSimulator(base_simulator.BaseSimulator):
    method __init__ (line 100) | def __init__(self, config: config_classes.EmulatorConfig):
    method _reconnect_on_grpc_error (line 166) | def _reconnect_on_grpc_error(func):
    method get_logs (line 181) | def get_logs(self) -> str:
    method adb_device_name (line 189) | def adb_device_name(self) -> str:
    method create_adb_controller (line 192) | def create_adb_controller(self):
    method create_log_stream (line 196) | def create_log_stream(self) -> log_stream.LogStream:
    method _launch_impl (line 202) | def _launch_impl(self) -> None:
    method load_state (line 251) | def load_state(
    method save_state (line 299) | def save_state(
    method _connect_to_emulator (line 331) | def _connect_to_emulator(
    method _confirm_booted (line 363) | def _confirm_booted(self, startup_wait_time_sec: int = 300):
    method send_touch (line 394) | def send_touch(self, touches: list[tuple[int, int, bool, int]]) -> None:
    method send_key (line 418) | def send_key(self, keycode: np.int32, event_type: str) -> None:
    method _get_screenshot_impl (line 446) | def _get_screenshot_impl(self) -> np.ndarray:
    method _shutdown_emulator (line 460) | def _shutdown_emulator(self):
    method close (line 475) | def close(self):

FILE: android_env/components/simulators/emulator/emulator_simulator_test.py
  class EmulatorSimulatorTest (line 39) | class EmulatorSimulatorTest(absltest.TestCase):
    method setUp (line 41) | def setUp(self):
    method test_adb_device_name_not_empty (line 77) | def test_adb_device_name_not_empty(self):
    method test_logfile_path (line 90) | def test_logfile_path(self):
    method test_grpc_port (line 115) | def test_grpc_port(self, unused_mock_portpicker):
    method test_grpc_port_unavailable (line 131) | def test_grpc_port_unavailable(self, unused_mock_portpicker):
    method test_launch_operation_order (line 146) | def test_launch_operation_order(self):
    method test_close (line 175) | def test_close(self):
    method test_value_error_if_launch_attempt_params_incorrect (line 195) | def test_value_error_if_launch_attempt_params_incorrect(self):
    method test_launch_attempt_reboot (line 212) | def test_launch_attempt_reboot(self):
    method test_launch_attempt_reinstall_after_zero_attempts (line 238) | def test_launch_attempt_reinstall_after_zero_attempts(self):
    method test_launch_attempt_reinstall (line 265) | def test_launch_attempt_reinstall(self):
    method test_get_screenshot (line 297) | def test_get_screenshot(self):
    method test_load_state (line 323) | def test_load_state(self):
    method test_save_state (line 377) | def test_save_state(self):
    method test_send_touch (line 419) | def test_send_touch(self):
    method test_send_key (line 476) | def test_send_key(self):

FILE: android_env/components/simulators/fake/fake_simulator.py
  class FakeStream (line 30) | class FakeStream:
    method __init__ (line 33) | def __init__(self):
    method _make_stdout (line 44) | def _make_stdout(self, data):
    method kill (line 48) | def kill(self):
    method __iter__ (line 51) | def __iter__(self):
  class FakeLogStream (line 63) | class FakeLogStream(log_stream.LogStream):
    method __init__ (line 66) | def __init__(self):
    method _get_stream_output (line 70) | def _get_stream_output(self):
    method stop_stream (line 73) | def stop_stream(self):
  class FakeAdbController (line 77) | class FakeAdbController(adb_controller.AdbController):
    method execute_command (line 80) | def execute_command(
  class FakeSimulator (line 106) | class FakeSimulator(base_simulator.BaseSimulator):
    method __init__ (line 109) | def __init__(self, config: config_classes.FakeSimulatorConfig):
    method get_logs (line 115) | def get_logs(self) -> str:
    method adb_device_name (line 118) | def adb_device_name(self) -> str:
    method create_adb_controller (line 121) | def create_adb_controller(self):
    method create_log_stream (line 124) | def create_log_stream(self) -> log_stream.LogStream:
    method _launch_impl (line 127) | def _launch_impl(self) -> None:
    method send_touch (line 130) | def send_touch(self, touches: list[tuple[int, int, bool, int]]) -> None:
    method send_key (line 133) | def send_key(self, keycode: np.int32, event_type: str) -> None:
    method _get_screenshot_impl (line 136) | def _get_screenshot_impl(self) -> np.ndarray:

FILE: android_env/components/simulators/fake/fake_simulator_test.py
  class FakeSimulatorTest (line 25) | class FakeSimulatorTest(absltest.TestCase):
    method test_device_name (line 27) | def test_device_name(self):
    method test_launch_close (line 33) | def test_launch_close(self):
    method test_get_screenshot (line 42) | def test_get_screenshot(self):
    method test_log_stream (line 52) | def test_log_stream(self):
    method test_adb_output (line 72) | def test_adb_output(self):
    method test_send_touch (line 92) | def test_send_touch(self):
    method test_send_key (line 102) | def test_send_key(self):

FILE: android_env/components/specs.py
  function base_action_spec (line 45) | def base_action_spec(
  function base_observation_spec (line 102) | def base_observation_spec(height: int, width: int) -> dict[str, specs.Ar...

FILE: android_env/components/specs_test.py
  class SpecsTest (line 26) | class SpecsTest(parameterized.TestCase):
    method test_base_action_spec (line 28) | def test_base_action_spec(self):
    method test_base_action_spec_with_key_events (line 37) | def test_base_action_spec_with_key_events(self):
    method test_base_action_spec_multitouch (line 48) | def test_base_action_spec_multitouch(self):
    method test_base_observation_spec (line 71) | def test_base_observation_spec(self, height, width):

FILE: android_env/components/task_manager.py
  class TaskManager (line 42) | class TaskManager:
    method __init__ (line 45) | def __init__(
    method stats (line 89) | def stats(self) -> dict[str, Any]:
    method setup_task (line 99) | def setup_task(self) -> None:
    method stop (line 103) | def stop(self) -> None:
    method start (line 116) | def start(
    method reset_task (line 128) | def reset_task(self) -> None:
    method rl_reset (line 149) | def rl_reset(self, observation: dict[str, Any]) -> dm_env.TimeStep:
    method rl_step (line 167) | def rl_step(self, observation: dict[str, Any]) -> dm_env.TimeStep:
    method _get_current_reward (line 182) | def _get_current_reward(self) -> float:
    method _get_current_extras (line 188) | def _get_current_extras(self) -> dict[str, Any]:
    method _determine_transition_fn (line 196) | def _determine_transition_fn(self) -> Callable[..., dm_env.TimeStep]:
    method _start_setup_step_interpreter (line 236) | def _start_setup_step_interpreter(
    method _start_logcat_thread (line 243) | def _start_logcat_thread(self, log_stream: log_stream_lib.LogStream):
    method _start_dumpsys_thread (line 250) | def _start_dumpsys_thread(
    method _stop_logcat_thread (line 262) | def _stop_logcat_thread(self):
    method _increment_bad_state (line 267) | def _increment_bad_state(self) -> None:
    method _logcat_listeners (line 287) | def _logcat_listeners(self) -> Iterable[logcat_thread.EventListener]:
    method _reward_listeners (line 301) | def _reward_listeners(
    method _reward_event_listeners (line 317) | def _reward_event_listeners(
    method _score_listeners (line 338) | def _score_listeners(
    method _episode_end_listeners (line 355) | def _episode_end_listeners(
    method _process_extra (line 370) | def _process_extra(self, extra_name: str, extra: Sequence[int | float]):
    method _extras_listeners (line 386) | def _extras_listeners(
    method _json_extras_listeners (line 418) | def _json_extras_listeners(

FILE: android_env/components/task_manager_test.py
  class TaskManagerTest (line 32) | class TaskManagerTest(absltest.TestCase):
    method setUp (line 34) | def setUp(self):
    method test_start (line 58) | def test_start(self):
    method test_setup_task (line 66) | def test_setup_task(self):
    method test_step_count (line 73) | def test_step_count(self):
    method test_get_current_reward (line 87) | def test_get_current_reward(self):
    method test_reward_event (line 114) | def test_reward_event(self):
    method test_get_current_reward_via_score (line 149) | def test_get_current_reward_via_score(self):
    method test_get_current_extras (line 180) | def test_get_current_extras(self):
    method test_get_current_extras_json_format (line 224) | def test_get_current_extras_json_format(self):
    method test_get_current_extras_failed_to_parse (line 287) | def test_get_current_extras_failed_to_parse(self):
    method test_multi_log_regexp (line 330) | def test_multi_log_regexp(self):
    method test_multi_reward_regexp (line 357) | def test_multi_reward_regexp(self):
    method test_determine_transition_fn (line 388) | def test_determine_transition_fn(self):

FILE: android_env/env_interface.py
  class AndroidEnvInterface (line 32) | class AndroidEnvInterface(dm_env.Environment, metaclass=abc.ABCMeta):
    method action_spec (line 38) | def action_spec(self) -> dict[str, specs.Array]:
    method observation_spec (line 42) | def observation_spec(self) -> dict[str, specs.Array]:
    method reset (line 46) | def reset(self) -> dm_env.TimeStep:
    method step (line 50) | def step(self, action: dict[str, np.ndarray]) -> dm_env.TimeStep:
    method close (line 54) | def close(self) -> None:
    method task_extras (line 59) | def task_extras(self, latest_only: bool = True) -> dict[str, np.ndarray]:
    method raw_action (line 65) | def raw_action(self) -> Any:
    method raw_observation (line 69) | def raw_observation(self) -> Any:
    method stats (line 72) | def stats(self) -> dict[str, Any]:
    method execute_adb_call (line 77) | def execute_adb_call(self, call: adb_pb2.AdbRequest) -> adb_pb2.AdbRes...
    method load_state (line 82) | def load_state(
    method save_state (line 97) | def save_state(

FILE: android_env/environment.py
  class AndroidEnv (line 32) | class AndroidEnv(env_interface.AndroidEnvInterface):
    method __init__ (line 35) | def __init__(
    method __del__ (line 55) | def __del__(self) -> None:
    method action_spec (line 60) | def action_spec(self) -> dict[str, dm_env.specs.Array]:
    method observation_spec (line 63) | def observation_spec(self) -> dict[str, dm_env.specs.Array]:
    method reset (line 66) | def reset(self) -> dm_env.TimeStep:
    method step (line 90) | def step(self, action: dict[str, np.ndarray]) -> dm_env.TimeStep:
    method close (line 116) | def close(self) -> None:
    method task_extras (line 127) | def task_extras(self, latest_only: bool = True) -> dict[str, np.ndarray]:
    method raw_action (line 137) | def raw_action(self):
    method raw_observation (line 141) | def raw_observation(self):
    method stats (line 144) | def stats(self) -> dict[str, Any]:
    method execute_adb_call (line 149) | def execute_adb_call(self, call: adb_pb2.AdbRequest) -> adb_pb2.AdbRes...
    method load_state (line 152) | def load_state(
    method save_state (line 176) | def save_state(

FILE: android_env/environment_test.py
  function _create_mock_coordinator (line 33) | def _create_mock_coordinator() -> coordinator_lib.Coordinator:
  function _create_fake_simulator (line 50) | def _create_fake_simulator() -> fake_simulator.FakeSimulator:
  class AndroidEnvTest (line 56) | class AndroidEnvTest(absltest.TestCase):
    method test_specs (line 58) | def test_specs(self):
    method test_reset_and_step (line 93) | def test_reset_and_step(self):
    method test_adb_call (line 208) | def test_adb_call(self):
    method test_load_state (line 226) | def test_load_state(self):
    method test_save_state (line 244) | def test_save_state(self):
    method test_double_close (line 260) | def test_double_close(self):

FILE: android_env/loader.py
  function _load_task (line 33) | def _load_task(task_config: config_classes.TaskConfig) -> task_pb2.Task:
  function load (line 47) | def load(config: config_classes.AndroidEnvConfig) -> environment.Android...
  function _process_emulator_launcher_config (line 71) | def _process_emulator_launcher_config(

FILE: android_env/loader_test.py
  class LoaderTest (line 34) | class LoaderTest(absltest.TestCase):
    method test_load_emulator (line 41) | def test_load_emulator(
    method test_load_fake_simulator (line 103) | def test_load_fake_simulator(
    method test_task (line 142) | def test_task(

FILE: android_env/wrappers/a11y/a11y_events.py
  function package_events_to_task_extras (line 31) | def package_events_to_task_extras(
  function extract_events_from_task_extras (line 40) | def extract_events_from_task_extras(
  function keep_latest_event_only (line 101) | def keep_latest_event_only(task_extras: dict[str, Any]):

FILE: android_env/wrappers/a11y/a11y_events_test.py
  function _event_request (line 27) | def _event_request(d: dict[str, str]) -> a11y_pb2.EventRequest:
  function _event_request_as_any (line 34) | def _event_request_as_any(d: dict[str, str]) -> any_pb2.Any:
  class A11yEventsTest (line 41) | class A11yEventsTest(parameterized.TestCase):
    method test_no_events_in_task_extras (line 55) | def test_no_events_in_task_extras(self, task_extras):
    method test_task_extras (line 76) | def test_task_extras(self, task_extras, expected_events):
    method test_events_key_has_dict_event_requrests (line 87) | def test_events_key_has_dict_event_requrests(self):
    method test_events_key_has__event_requrests_packed_as_any (line 107) | def test_events_key_has__event_requrests_packed_as_any(self):
    method test_events_key_has_non_event_requrests (line 127) | def test_events_key_has_non_event_requrests(self):
    method test_keep_latest_only (line 160) | def test_keep_latest_only(self, task_extras, expected_extras):

FILE: android_env/wrappers/a11y/a11y_forests.py
  function package_forests_to_task_extras (line 30) | def package_forests_to_task_extras(
  function task_extras_has_forests (line 39) | def task_extras_has_forests(task_extras: Mapping[str, Any]) -> bool:
  function convert_to_forest (line 64) | def convert_to_forest(
  function extract_forests_from_task_extras (line 87) | def extract_forests_from_task_extras(
  function keep_latest_forest_only (line 113) | def keep_latest_forest_only(task_extras: dict[str, Any]):

FILE: android_env/wrappers/a11y/a11y_forests_test.py
  function _pack_any (line 27) | def _pack_any(proto_message) -> any_pb2.Any:
  function _empty_forest (line 33) | def _empty_forest() -> (
  function _one_empty_window_forest (line 39) | def _one_empty_window_forest() -> (
  function _two_window_forest (line 47) | def _two_window_forest() -> (
  class A11YForestsTest (line 59) | class A11YForestsTest(parameterized.TestCase):
    method test_task_extras (line 156) | def test_task_extras(self, task_extras, expected_forests, convert_to_np):
    method test_keep_latest_only (line 215) | def test_keep_latest_only(self, task_extras, expected_extras):

FILE: android_env/wrappers/a11y/a11y_servicer.py
  class A11yServicer (line 29) | class A11yServicer(a11y_pb2_grpc.A11yServiceServicer):
    method __init__ (line 32) | def __init__(self, latest_forest_only: bool = False):
    method SendForest (line 49) | def SendForest(
    method SendEvent (line 57) | def SendEvent(
    method Bidi (line 65) | async def Bidi(
    method get_forest (line 135) | async def get_forest(
    method gather_forests (line 145) | def gather_forests(
    method gather_events (line 154) | def gather_events(self) -> list[a11y_pb2.EventRequest]:
    method pause_and_clear (line 161) | def pause_and_clear(self) -> None:
    method resume (line 178) | def resume(self) -> None:
    method _process_event (line 182) | def _process_event(self, event: a11y_pb2.EventRequest) -> None:
    method _process_forest (line 189) | def _process_forest(

FILE: android_env/wrappers/a11y/a11y_servicer_test.py
  function _aiter (line 34) | async def _aiter(xs: Iterable[_T]) -> AsyncIterator[_T]:
  function one_window_one_node_forest (line 41) | def one_window_one_node_forest() -> (
  function one_window_two_nodes_forest (line 53) | def one_window_two_nodes_forest() -> (
  function empty_dict (line 69) | def empty_dict() -> dict[str, str]:
  function single_item_dict_with_special_chars (line 73) | def single_item_dict_with_special_chars() -> dict[str, str]:
  class A11yServicerTest (line 77) | class A11yServicerTest(parameterized.TestCase, IsolatedAsyncioTestCase):
    method test_servicer_sendforest (line 79) | def test_servicer_sendforest(self):
    method test_servicer_bidi_forests (line 92) | async def test_servicer_bidi_forests(self):
    method test_servicer_sendforest_latest_only (line 123) | def test_servicer_sendforest_latest_only(self):
    method test_servicer_sendevent (line 135) | def test_servicer_sendevent(self):
    method test_servicer_bidi_events (line 153) | async def test_servicer_bidi_events(self):
    method test_servicer_pause_and_clear_pauses (line 187) | def test_servicer_pause_and_clear_pauses(self):
    method test_servicer_pause_and_clear_clears (line 203) | def test_servicer_pause_and_clear_clears(self):

FILE: android_env/wrappers/a11y_grpc_wrapper.py
  function _get_accessibility_forwarder_apk (line 39) | def _get_accessibility_forwarder_apk() -> bytes:
  class EnableNetworkingError (line 47) | class EnableNetworkingError(ValueError):
  class A11yGrpcWrapper (line 51) | class A11yGrpcWrapper(base_wrapper.BaseWrapper):
    method __init__ (line 79) | def __init__(
    method _start_a11y_services (line 169) | def _start_a11y_services(self) -> None:
    method _install_a11y_forwarding_apk (line 195) | def _install_a11y_forwarding_apk(self) -> None:
    method _enable_a11y_tree_logs (line 210) | def _enable_a11y_tree_logs(self) -> None:
    method _reset_enable_networking_attempts (line 231) | def _reset_enable_networking_attempts(self) -> None:
    method get_port (line 236) | def get_port(self):
    method close (line 239) | def close(self):
    method attempt_enable_networking (line 244) | def attempt_enable_networking(self) -> None:
    method _configure_grpc (line 276) | def _configure_grpc(self) -> None:
    method _accumulate_and_return_a11y_info (line 346) | def _accumulate_and_return_a11y_info(
    method _fetch_task_extras_and_update_observation (line 385) | def _fetch_task_extras_and_update_observation(
    method reset (line 400) | def reset(self) -> dm_env.TimeStep:
    method step (line 416) | def step(self, action: Any) -> dm_env.TimeStep:
    method accumulate_new_extras (line 425) | def accumulate_new_extras(self) -> dict[str, Any]:
    method _fetch_task_extras (line 440) | def _fetch_task_extras(self) -> dict[str, Any]:
    method task_extras (line 499) | def task_extras(self, latest_only: bool = False) -> dict[str, Any]:

FILE: android_env/wrappers/a11y_grpc_wrapper_test.py
  function empty_forest (line 34) | def empty_forest() -> (
  function one_empty_window_forest (line 40) | def one_empty_window_forest() -> (
  function one_window_one_node_forest (line 48) | def one_window_one_node_forest() -> (
  function one_window_two_nodes_forest (line 60) | def one_window_two_nodes_forest() -> (
  function three_windows_forest (line 76) | def three_windows_forest() -> (
  function empty_dict (line 98) | def empty_dict() -> dict[str, str]:
  function single_item_dict (line 102) | def single_item_dict() -> dict[str, str]:
  function several_long_items_dict (line 106) | def several_long_items_dict() -> dict[str, str]:
  function single_item_dict_with_special_chars (line 113) | def single_item_dict_with_special_chars() -> dict[str, str]:
  function _ok_response (line 117) | def _ok_response():
  class A11yGrpcWrapperTest (line 121) | class A11yGrpcWrapperTest(parameterized.TestCase):
    method test_server (line 123) | def test_server(self):
    method test_fetch_task_extras (line 169) | def test_fetch_task_extras(
    method test_fetch_task_extras_enable_networking (line 226) | def test_fetch_task_extras_enable_networking(
    method test_fetch_task_extras_enable_networking_twice (line 260) | def test_fetch_task_extras_enable_networking_twice(
    method test_task_extras_raises_a11y_info_exception (line 330) | def test_task_extras_raises_a11y_info_exception(
    method test_configure_grpc (line 380) | def test_configure_grpc(
    method test_task_extras_raises_before_reset (line 407) | def test_task_extras_raises_before_reset(
    method test_extras_accumulate_between_steps (line 426) | def test_extras_accumulate_between_steps(
    method test_a11y_info_disabled (line 521) | def test_a11y_info_disabled(
    method test_a11y_info_with_timer_info_present (line 554) | def test_a11y_info_with_timer_info_present(
    method test_a11y_info_with_timer_task_extra_returned (line 587) | def test_a11y_info_with_timer_task_extra_returned(
    method test_a11y_info_with_timer_from_action (line 622) | def test_a11y_info_with_timer_from_action(
    method test_task_extras_same_between_calls (line 657) | def test_task_extras_same_between_calls(self, mock_server, mock_add_se...
    method test_task_extras_clear_if_called_between_step (line 721) | def test_task_extras_clear_if_called_between_step(
    method test_apk_install_and_start (line 789) | def test_apk_install_and_start(
    method test_component_and_start (line 820) | def test_component_and_start(self, unused_mock_sleep):
    method test_broadcast_sent_default_grpc_server_ip (line 847) | def test_broadcast_sent_default_grpc_server_ip(self):
    method test_broadcast_sent_custom_grpc_server_ip (line 875) | def test_broadcast_sent_custom_grpc_server_ip(self, grpc_server_ip):
    method test_broadcast_sent_port (line 902) | def test_broadcast_sent_port(self):

FILE: android_env/wrappers/base_wrapper.py
  class BaseWrapper (line 29) | class BaseWrapper(env_interface.AndroidEnvInterface):
    method __init__ (line 32) | def __init__(self, env: env_interface.AndroidEnvInterface) -> None:
    method reset (line 36) | def reset(self) -> dm_env.TimeStep:
    method step (line 41) | def step(self, action: Any) -> dm_env.TimeStep:
    method task_extras (line 45) | def task_extras(self, latest_only: bool = True) -> dict[str, np.ndarray]:
    method _reset_state (line 48) | def _reset_state(self):
    method _process_action (line 51) | def _process_action(self, action: Any) -> Any:
    method _process_timestep (line 54) | def _process_timestep(self, timestep: dm_env.TimeStep) -> dm_env.TimeS...
    method observation_spec (line 57) | def observation_spec(self) -> dict[str, specs.Array]:
    method action_spec (line 60) | def action_spec(self) -> dict[str, specs.Array]:
    method reward_spec (line 63) | def reward_spec(self) -> specs.Array:
    method discount_spec (line 66) | def discount_spec(self) -> specs.Array:
    method _wrapper_stats (line 69) | def _wrapper_stats(self) -> dict[str, Any]:
    method stats (line 73) | def stats(self) -> dict[str, Any]:
    method load_state (line 78) | def load_state(
    method save_state (line 84) | def save_state(
    method execute_adb_call (line 99) | def execute_adb_call(
    method raw_action (line 105) | def raw_action(self) -> Any:
    method raw_observation (line 109) | def raw_observation(self) -> Any:
    method raw_env (line 113) | def raw_env(self) -> env_interface.AndroidEnvInterface:
    method __getattr__ (line 120) | def __getattr__(self, attr) -> Any:
    method close (line 124) | def close(self) -> None:

FILE: android_env/wrappers/base_wrapper_test.py
  class BaseWrapperTest (line 27) | class BaseWrapperTest(absltest.TestCase):
    method test_base_function_forwarding (line 30) | def test_base_function_forwarding(self, mock_info):
    method test_multiple_wrappers (line 95) | def test_multiple_wrappers(self):
    method test_raw_env (line 103) | def test_raw_env(self):
    method test_stats (line 109) | def test_stats(self):
    method test_wrapped_stats (line 117) | def test_wrapped_stats(self, mock_info):

FILE: android_env/wrappers/discrete_action_wrapper.py
  class DiscreteActionWrapper (line 32) | class DiscreteActionWrapper(base_wrapper.BaseWrapper):
    method __init__ (line 35) | def __init__(
    method _assert_base_env (line 54) | def _assert_base_env(self) -> None:
    method num_actions (line 62) | def num_actions(self) -> int:
    method step (line 70) | def step(self, action: dict[str, int]) -> dm_env.TimeStep:
    method _process_action (line 75) | def _process_action(self, action: dict[str, int]) -> dict[str, np.ndar...
    method _get_action_type (line 87) | def _get_action_type(self, action_id: int) -> action_type.ActionType:
    method _get_touch_position (line 118) | def _get_touch_position(self, action_id: int) -> Sequence[float]:
    method action_spec (line 162) | def action_spec(self) -> dict[str, specs.Array]:

FILE: android_env/wrappers/discrete_action_wrapper_test.py
  function _make_array_spec (line 30) | def _make_array_spec(shape, dtype, name):
  function _valid_shape (line 41) | def _valid_shape(action):
  function _valid_types (line 51) | def _valid_types(action, types):
  class DiscreteActionWrapperTest (line 56) | class DiscreteActionWrapperTest(absltest.TestCase):
    method setUp (line 58) | def setUp(self):
    method test_num_actions (line 70) | def test_num_actions(self):
    method test_num_actions_non_redundant (line 76) | def test_num_actions_non_redundant(self):
    method test_reset (line 84) | def test_reset(self):
    method test_step_no_noise (line 93) | def test_step_no_noise(self):
    method test_step_redundant_actions_invalid_action_id (line 158) | def test_step_redundant_actions_invalid_action_id(self):
    method test_step_no_noise_no_redudant_actions (line 167) | def test_step_no_noise_no_redudant_actions(self):
    method test_step_no_redundant_actions_invalid_action_id (line 227) | def test_step_no_redundant_actions_invalid_action_id(self):
    method test_step_with_noise (line 236) | def test_step_with_noise(self):
    method test_parent_spec_type (line 291) | def test_parent_spec_type(self):
    method test_observation_spec (line 315) | def test_observation_spec(self):
    method test_action_spec (line 324) | def test_action_spec(self):
    method test_action_spec_non_redundant (line 334) | def test_action_spec_non_redundant(self):
    method test_assert_base_env_action_spec_too_short (line 345) | def test_assert_base_env_action_spec_too_short(self):
    method test_assert_base_env_action_spec_too_long (line 353) | def test_assert_base_env_action_spec_too_long(self):
    method test_assert_base_env_action_spec_wrong_shapes (line 365) | def test_assert_base_env_action_spec_wrong_shapes(self):
    method test_assert_base_env_ok (line 375) | def test_assert_base_env_ok(self):

FILE: android_env/wrappers/flat_interface_wrapper.py
  function _extract_screen_pixels (line 29) | def _extract_screen_pixels(obs: np.ndarray) -> np.ndarray:
  function _get_no_action_observation_spec (line 37) | def _get_no_action_observation_spec(
  class FlatInterfaceWrapper (line 53) | class FlatInterfaceWrapper(base_wrapper.BaseWrapper):
    method __init__ (line 62) | def __init__(
    method _assert_base_env (line 76) | def _assert_base_env(self) -> None:
    method _process_action (line 82) | def _process_action(
    method _process_timestep (line 90) | def _process_timestep(self, timestep: dm_env.TimeStep) -> dm_env.TimeS...
    method reset (line 107) | def reset(self) -> dm_env.TimeStep:
    method step (line 111) | def step(self, action: int) -> dm_env.TimeStep:
    method observation_spec (line 115) | def observation_spec(self) -> specs.Array | dict[str, specs.Array]:  #...
    method action_spec (line 126) | def action_spec(self) -> specs.BoundedArray | dict[str, specs.Array]: ...

FILE: android_env/wrappers/flat_interface_wrapper_test.py
  function _make_array_spec (line 28) | def _make_array_spec(shape, dtype=np.float32, name=None, maximum=3, mini...
  function _make_timestep (line 37) | def _make_timestep(observation):
  class FlatInterfaceWrapperTest (line 46) | class FlatInterfaceWrapperTest(absltest.TestCase):
    method setUp (line 48) | def setUp(self):
    method test_reset (line 86) | def test_reset(self):
    method test_reset_no_action_layer (line 92) | def test_reset_no_action_layer(self):
    method test_step (line 101) | def test_step(self):
    method test_step_no_action_layer (line 114) | def test_step_no_action_layer(self):
    method test_observation_spec (line 131) | def test_observation_spec(self):
    method test_observation_spec_no_action_layer (line 137) | def test_observation_spec_no_action_layer(self):
    method test_action_spec (line 145) | def test_action_spec(self):
    method test_bad_action_spec_structured_action (line 155) | def test_bad_action_spec_structured_action(self):

FILE: android_env/wrappers/float_pixels_wrapper.py
  class FloatPixelsWrapper (line 26) | class FloatPixelsWrapper(base_wrapper.BaseWrapper):
    method __init__ (line 29) | def __init__(self, env: env_interface.AndroidEnvInterface) -> None:
    method _process_observation (line 35) | def _process_observation(
    method _process_timestep (line 45) | def _process_timestep(self, timestep: dm_env.TimeStep) -> dm_env.TimeS...
    method reset (line 53) | def reset(self) -> dm_env.TimeStep:
    method step (line 56) | def step(self, action: dict[str, np.ndarray]) -> dm_env.TimeStep:
    method observation_spec (line 59) | def observation_spec(self) -> dict[str, specs.Array]:

FILE: android_env/wrappers/float_pixels_wrapper_test.py
  function _make_array_spec (line 27) | def _make_array_spec(shape, dtype=np.float32, name=None):
  function _make_bounded_array_spec (line 35) | def _make_bounded_array_spec(
  function _simple_timestep (line 46) | def _simple_timestep(obs_shape, obs_type):
  class FloatPixelsWrapperTest (line 55) | class FloatPixelsWrapperTest(absltest.TestCase):
    method setUp (line 57) | def setUp(self):
    method test_float_pixels_wrapper_spec (line 81) | def test_float_pixels_wrapper_spec(self):
    method test_float_pixels_wrapper_step (line 97) | def test_float_pixels_wrapper_step(self):
    method test_float_pixels_wrapper_reset (line 111) | def test_float_pixels_wrapper_reset(self):
    method test_float_pixels_wrapper_already_float (line 125) | def test_float_pixels_wrapper_already_float(self):

FILE: android_env/wrappers/gym_wrapper.py
  class GymInterfaceWrapper (line 28) | class GymInterfaceWrapper(gym.Env):
    method __init__ (line 31) | def __init__(self, env: dm_env.Environment):
    method _spec_to_space (line 39) | def _spec_to_space(self, spec: specs.Array) -> spaces.Space:
    method render (line 75) | def render(self, mode='rgb_array'):
    method reset (line 85) | def reset(self) -> np.ndarray:
    method step (line 90) | def step(self, action: dict[str, int]) -> tuple[Any, ...]:

FILE: android_env/wrappers/gym_wrapper_test.py
  class GymInterfaceWrapperTest (line 29) | class GymInterfaceWrapperTest(absltest.TestCase):
    method setUp (line 31) | def setUp(self):
    method test_render (line 68) | def test_render(self):
    method test_render_error (line 74) | def test_render_error(self):
    method test_reset (line 78) | def test_reset(self):
    method test_step (line 88) | def test_step(self):
    method test_spec_to_space (line 94) | def test_spec_to_space(self):

FILE: android_env/wrappers/image_rescale_wrapper.py
  class ImageRescaleWrapper (line 36) | class ImageRescaleWrapper(base_wrapper.BaseWrapper):
    method __init__ (line 39) | def __init__(
    method _process_timestep (line 58) | def _process_timestep(self, timestep: dm_env.TimeStep) -> dm_env.TimeS...
    method _process_pixels (line 66) | def _process_pixels(self, raw_observation: np.ndarray) -> np.ndarray:
    method _resize_image_array (line 79) | def _resize_image_array(
    method reset (line 94) | def reset(self) -> dm_env.TimeStep:
    method step (line 98) | def step(self, action) -> dm_env.TimeStep:
    method observation_spec (line 102) | def observation_spec(self) -> dict[str, specs.Array]:

FILE: android_env/wrappers/image_rescale_wrapper_test.py
  function _simple_spec (line 29) | def _simple_spec():
  function _simple_timestep (line 38) | def _simple_timestep():
  class ImageRescaleWrapperTest (line 47) | class ImageRescaleWrapperTest(absltest.TestCase):
    method test_100x50_grayscale (line 49) | def test_100x50_grayscale(self):
    method test_150x60_full_channels (line 67) | def test_150x60_full_channels(self):
    method test_list_zoom_factor (line 85) | def test_list_zoom_factor(self):

FILE: android_env/wrappers/last_action_wrapper.py
  class LastActionWrapper (line 28) | class LastActionWrapper(base_wrapper.BaseWrapper):
    method __init__ (line 39) | def __init__(self,
    method _process_timestep (line 53) | def _process_timestep(self, timestep: dm_env.TimeStep) -> dm_env.TimeS...
    method _process_observation (line 58) | def _process_observation(
    method _get_last_action_layer (line 72) | def _get_last_action_layer(self, pixels: np.ndarray) -> np.ndarray:
    method reset (line 88) | def reset(self) -> dm_env.TimeStep:
    method step (line 92) | def step(self, action) -> dm_env.TimeStep:
    method observation_spec (line 96) | def observation_spec(self) -> dict[str, specs.Array]:

FILE: android_env/wrappers/last_action_wrapper_test.py
  function _simple_spec (line 30) | def _simple_spec():
  function _simple_timestep (line 39) | def _simple_timestep():
  class LastActionWrapperTest (line 48) | class LastActionWrapperTest(absltest.TestCase):
    method test_concat_to_pixels (line 50) | def test_concat_to_pixels(self):
    method test_no_concat_to_pixels (line 105) | def test_no_concat_to_pixels(self):

FILE: android_env/wrappers/rate_limit_wrapper.py
  class RateLimitWrapper (line 28) | class RateLimitWrapper(base_wrapper.BaseWrapper):
    class SleepType (line 31) | class SleepType(enum.IntEnum):
    method __init__ (line 49) | def __init__(self,
    method _assert_base_env (line 68) | def _assert_base_env(self):
    method reset (line 75) | def reset(self):
    method step (line 80) | def step(self, action: dict[str, np.ndarray]) -> dm_env.TimeStep:
    method _wait (line 112) | def _wait(self) -> None:

FILE: android_env/wrappers/rate_limit_wrapper_test.py
  function _get_base_env (line 32) | def _get_base_env():
  class _FnWithTimestamps (line 50) | class _FnWithTimestamps(Protocol):
  function _with_timestamp (line 57) | def _with_timestamp(fn: Any) -> _FnWithTimestamps:
  class RateLimitWrapperTest (line 61) | class RateLimitWrapperTest(parameterized.TestCase):
    method test_disabled (line 68) | def test_disabled(self, rate, mock_sleep):
    method test_enabled (line 83) | def test_enabled(self, mock_sleep):
    method test_enabled_sleep_type_before (line 110) | def test_enabled_sleep_type_before(self, mock_sleep):
    method test_enabled_sleep_type_after (line 150) | def test_enabled_sleep_type_after(self, mock_sleep):
    method test_enabled_sleep_type_after_with_repeat (line 189) | def test_enabled_sleep_type_after_with_repeat(self, mock_sleep):
    method test_enabled_sleep_type_after_with_repeat_last (line 244) | def test_enabled_sleep_type_after_with_repeat_last(self, mock_sleep):

FILE: android_env/wrappers/tap_action_wrapper.py
  class TapActionWrapper (line 28) | class TapActionWrapper(base_wrapper.BaseWrapper):
    method __init__ (line 31) | def __init__(self,
    method stats (line 41) | def stats(self) -> dict[str, Any]:
    method _process_action (line 47) | def _process_action(
    method step (line 76) | def step(self, action: dict[str, np.ndarray]) -> dm_env.TimeStep:
    method action_spec (line 97) | def action_spec(self) -> dict[str, dm_env.specs.Array]:

FILE: android_env/wrappers/tap_action_wrapper_test.py
  function _make_array_spec (line 29) | def _make_array_spec(shape, dtype, name):
  class TapActionWrapperTest (line 39) | class TapActionWrapperTest(absltest.TestCase):
    method setUp (line 41) | def setUp(self):
    method test_process_action_repeat (line 52) | def test_process_action_repeat(self):
    method test_process_action_lift (line 63) | def test_process_action_lift(self):
    method test_process_action_touch (line 74) | def test_process_action_touch(self):
    method test_reset (line 87) | def test_reset(self):
    method test_step (line 96) | def test_step(self):
    method test_observation_spec (line 122) | def test_observation_spec(self):
    method test_action_spec (line 131) | def test_action_spec(self):
    method test_stats (line 140) | def test_stats(self):

FILE: examples/run_acme_agent.py
  function apply_wrappers (line 50) | def apply_wrappers(env):
  function main (line 60) | def main(_):

FILE: examples/run_human_agent.py
  function _get_action_from_event (line 52) | def _get_action_from_event(
  function _get_action_from_mouse (line 69) | def _get_action_from_mouse(
  function _scale_position (line 86) | def _scale_position(position: np.ndarray, screen: pygame.Surface,
  function _accumulate_reward (line 97) | def _accumulate_reward(
  function _render_pygame_frame (line 114) | def _render_pygame_frame(surface: pygame.Surface, screen: pygame.Surface,
  function main (line 128) | def main(_):

FILE: examples/run_random_agent.py
  function main (line 46) | def main(_):

FILE: setup.py
  class _GenerateProtoFiles (line 48) | class _GenerateProtoFiles(setuptools.Command):
    method initialize_options (line 54) | def initialize_options(self):
    method finalize_options (line 57) | def finalize_options(self):
    method run (line 60) | def run(self):
  class _BuildExt (line 84) | class _BuildExt(build_ext):
    method run (line 87) | def run(self):
  class _BuildPy (line 92) | class _BuildPy(build_py):
    method run (line 95) | def run(self):
Condensed preview — 148 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (972K chars).
[
  {
    "path": ".github/workflows/tests.yml",
    "chars": 977,
    "preview": "name: tests\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n  workflow_dispatch:\n    inputs:\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 926,
    "preview": "\n# How to Contribute\n\n# Pull Requests\n\nPlease send in fixes or feature additions through Pull Requests.\n\n## Contributor "
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "README.md",
    "chars": 7799,
    "preview": "\n# AndroidEnv - The Android Learning Environment\n\n<img align=\"right\" src=\"docs/images/device_control.gif\" width=\"160\" he"
  },
  {
    "path": "android_env/__init__.py",
    "chars": 610,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/apps/MODULE.bazel",
    "chars": 2590,
    "preview": "# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# yo"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityForwarder.kt",
    "chars": 9447,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityForwarderTest.kt",
    "chars": 19384,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityTreeCreator.kt",
    "chars": 9535,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityTreeCreatorTest.kt",
    "chars": 3152,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/accessibilityforwarder/AndroidManifest.xml",
    "chars": 1954,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright 2026 DeepMind Technologies Limited.\n\n Licensed under the Apache Li"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/accessibilityforwarder/AndroidManifest_lite.xml",
    "chars": 856,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright 2026 DeepMind Technologies Limited.\n\n Licensed under the Apache Li"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/accessibilityforwarder/FlagsBroadcastReceiver.kt",
    "chars": 2568,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/accessibilityforwarder/FlagsBroadcastReceiverTest.kt",
    "chars": 4895,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/accessibilityforwarder/LogFlags.kt",
    "chars": 1162,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/accessibilityforwarder/ParentChildNodePair.kt",
    "chars": 1348,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/accessibilityforwarder/UniqueIdsGenerator.kt",
    "chars": 1143,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/accessibilityforwarder/res/xml/accessibility_forwarder_service.xml",
    "chars": 958,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright 2026 DeepMind Technologies Limited.\n\n Licensed under the Apache Li"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/AndroidManifest.xml",
    "chars": 1540,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright 2026 DeepMind Technologies Limited.\n\n Licensed under the Apache Li"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/BUILD.bazel",
    "chars": 2185,
    "preview": "# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# yo"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/GameLogic.kt",
    "chars": 2560,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/GameLogicThread.kt",
    "chars": 1657,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/MainActivity.kt",
    "chars": 5060,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/RenderThread.kt",
    "chars": 2006,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/res/layout/main.xml",
    "chars": 1070,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright 2026 DeepMind Technologies Limited.\n\n Licensed under the Apache Li"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/res/values/strings.xml",
    "chars": 695,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright 2026 DeepMind Technologies Limited.\n\n Licensed under the Apache Li"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/sprite/BUILD.bazel",
    "chars": 1414,
    "preview": "# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# yo"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/sprite/Background.kt",
    "chars": 977,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/sprite/Ball.kt",
    "chars": 3947,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/sprite/LineSegment.kt",
    "chars": 790,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/sprite/Paddle.kt",
    "chars": 2116,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/sprite/Point.kt",
    "chars": 736,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/java/com/google/androidenv/catch/sprite/Sprite.kt",
    "chars": 845,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/javatests/com/google/androidenv/catch/AndroidManifest.xml",
    "chars": 1540,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright 2026 DeepMind Technologies Limited.\n\n Licensed under the Apache Li"
  },
  {
    "path": "android_env/apps/javatests/com/google/androidenv/catch/BUILD.bazel",
    "chars": 3032,
    "preview": "# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# yo"
  },
  {
    "path": "android_env/apps/javatests/com/google/androidenv/catch/GameLogicTest.kt",
    "chars": 6129,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/javatests/com/google/androidenv/catch/GameLogicThreadTest.kt",
    "chars": 2347,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/javatests/com/google/androidenv/catch/MainActivityTest.kt",
    "chars": 3227,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/javatests/com/google/androidenv/catch/RenderThreadTest.kt",
    "chars": 4631,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/javatests/com/google/androidenv/catch/sprite/BUILD.bazel",
    "chars": 2823,
    "preview": "# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# yo"
  },
  {
    "path": "android_env/apps/javatests/com/google/androidenv/catch/sprite/BackgroundTest.kt",
    "chars": 1780,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/javatests/com/google/androidenv/catch/sprite/BallTest.kt",
    "chars": 12955,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/javatests/com/google/androidenv/catch/sprite/PaddleTest.kt",
    "chars": 5638,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/apps/javatests/com/google/androidenv/catch/sprite/SpriteTest.kt",
    "chars": 1547,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/components/__init__.py",
    "chars": 610,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/action_fns.py",
    "chars": 4937,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/action_fns_test.py",
    "chars": 6383,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/action_type.py",
    "chars": 1697,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/adb_call_parser.py",
    "chars": 31768,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/adb_call_parser_test.py",
    "chars": 53188,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/adb_controller.py",
    "chars": 5896,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/adb_controller_test.py",
    "chars": 9713,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/adb_log_stream.py",
    "chars": 2071,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/adb_log_stream_test.py",
    "chars": 2028,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/app_screen_checker.py",
    "chars": 9629,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/app_screen_checker_test.py",
    "chars": 7608,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/config_classes.py",
    "chars": 7706,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/coordinator.py",
    "chars": 9747,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/coordinator_test.py",
    "chars": 11302,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/device_settings.py",
    "chars": 6341,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/device_settings_test.py",
    "chars": 7452,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/dumpsys_thread.py",
    "chars": 4696,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/dumpsys_thread_test.py",
    "chars": 3866,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/errors.py",
    "chars": 2509,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/errors_test.py",
    "chars": 3952,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/log_stream.py",
    "chars": 2096,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/log_stream_test.py",
    "chars": 3494,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/logcat_thread.py",
    "chars": 4884,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/logcat_thread_test.py",
    "chars": 5208,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/pixel_fns.py",
    "chars": 2417,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/pixel_fns_test.py",
    "chars": 3924,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/setup_step_interpreter.py",
    "chars": 6601,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/setup_step_interpreter_test.py",
    "chars": 11746,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/simulators/__init__.py",
    "chars": 610,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/simulators/base_simulator.py",
    "chars": 6900,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/simulators/base_simulator_test.py",
    "chars": 5805,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/simulators/emulator/__init__.py",
    "chars": 610,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/simulators/emulator/emulator_launcher.py",
    "chars": 5460,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/simulators/emulator/emulator_launcher_test.py",
    "chars": 9595,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/simulators/emulator/emulator_simulator.py",
    "chars": 17923,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/simulators/emulator/emulator_simulator_test.py",
    "chars": 19997,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/simulators/fake/__init__.py",
    "chars": 610,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/simulators/fake/fake_simulator.py",
    "chars": 4054,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/simulators/fake/fake_simulator_test.py",
    "chars": 4233,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/specs.py",
    "chars": 4965,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/specs_test.py",
    "chars": 3721,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/task_manager.py",
    "chars": 14754,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/components/task_manager_test.py",
    "chars": 17040,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/env_interface.py",
    "chars": 3306,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/environment.py",
    "chars": 6173,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/environment_test.py",
    "chars": 10865,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/loader.py",
    "chars": 3203,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/loader_test.py",
    "chars": 6389,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/proto/__init__.py",
    "chars": 610,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/proto/a11y/__init__.py",
    "chars": 610,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/proto/a11y/a11y.proto",
    "chars": 2333,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/proto/a11y/android_accessibility_action.proto",
    "chars": 1190,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/proto/a11y/android_accessibility_forest.proto",
    "chars": 1066,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/proto/a11y/android_accessibility_node_info.proto",
    "chars": 3618,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/proto/a11y/android_accessibility_node_info_clickable_span.proto",
    "chars": 1722,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/proto/a11y/android_accessibility_tree.proto",
    "chars": 1071,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/proto/a11y/android_accessibility_window_info.proto",
    "chars": 2679,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/proto/a11y/rect.proto",
    "chars": 975,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/proto/adb.proto",
    "chars": 12454,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/proto/emulator_controller.proto",
    "chars": 41025,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/proto/snapshot.proto",
    "chars": 5616,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/proto/snapshot_service.proto",
    "chars": 11893,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/proto/state.proto",
    "chars": 1835,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/proto/task.proto",
    "chars": 6712,
    "preview": "// Copyright 2026 DeepMind Technologies Limited.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n/"
  },
  {
    "path": "android_env/wrappers/__init__.py",
    "chars": 610,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/a11y/__init__.py",
    "chars": 610,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/a11y/a11y_events.py",
    "chars": 3614,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/a11y/a11y_events_test.py",
    "chars": 5771,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/a11y/a11y_forests.py",
    "chars": 3938,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/a11y/a11y_forests_test.py",
    "chars": 7438,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/a11y/a11y_servicer.py",
    "chars": 6658,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/a11y/a11y_servicer_test.py",
    "chars": 7546,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/a11y_grpc_wrapper.py",
    "chars": 19265,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/a11y_grpc_wrapper_test.py",
    "chars": 34383,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/base_wrapper.py",
    "chars": 3651,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/base_wrapper_test.py",
    "chars": 5562,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/discrete_action_wrapper.py",
    "chars": 6078,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/discrete_action_wrapper_test.py",
    "chars": 14387,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/flat_interface_wrapper.py",
    "chars": 4532,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/flat_interface_wrapper_test.py",
    "chars": 6126,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/float_pixels_wrapper.py",
    "chars": 2599,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/float_pixels_wrapper_test.py",
    "chars": 5312,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/gym_wrapper.py",
    "chars": 3077,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/gym_wrapper_test.py",
    "chars": 3949,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/image_rescale_wrapper.py",
    "chars": 4246,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/image_rescale_wrapper_test.py",
    "chars": 3931,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/last_action_wrapper.py",
    "chars": 4617,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/last_action_wrapper_test.py",
    "chars": 6346,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/rate_limit_wrapper.py",
    "chars": 4197,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/rate_limit_wrapper_test.py",
    "chars": 8940,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/tap_action_wrapper.py",
    "chars": 3641,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "android_env/wrappers/tap_action_wrapper_test.py",
    "chars": 5721,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "docs/emulator_guide.md",
    "chars": 3881,
    "preview": "# AndroidEnv - Emulator Setup Guide\n\nIn this document we provide a step-by-step guide for creating a virtual Android\ndev"
  },
  {
    "path": "docs/environment.md",
    "chars": 8685,
    "preview": "# AndroidEnv - Environment features\n\nAndroidEnv is a complex environment that, while offering an almost endless range\nof"
  },
  {
    "path": "docs/example_tasks.md",
    "chars": 69789,
    "preview": "# AndroidEnv - Available tasks\n\nThis page gives a detailed overview of the example tasks provided with\nAndroidEnv. The p"
  },
  {
    "path": "docs/instructions.md",
    "chars": 5587,
    "preview": "# AndroidEnv - Running the environment\n\nIn order to create an AndroidEnv instance you will need to provide two main\ncomp"
  },
  {
    "path": "docs/tasks_guide.md",
    "chars": 8170,
    "preview": "# AndroidEnv - Tasks\n\nWith AndroidEnv we provide a mechanism for easily defining RL tasks for the\nagent to learn. This i"
  },
  {
    "path": "examples/__init__.py",
    "chars": 610,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "examples/run_acme_agent.py",
    "chars": 3363,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "examples/run_human_agent.py",
    "chars": 6931,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "examples/run_random_agent.py",
    "chars": 3116,
    "preview": "# coding=utf-8\n# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "pyproject.toml",
    "chars": 971,
    "preview": "[build-system]\nrequires = [\n    \"setuptools\",\n    \"wheel\"\n]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"a"
  },
  {
    "path": "setup.py",
    "chars": 3595,
    "preview": "# Copyright 2026 DeepMind Technologies Limited.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# yo"
  }
]

About this extraction

This page contains the full source code of the google-deepmind/android_env GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 148 files (908.6 KB), approximately 221.9k tokens, and a symbol index with 809 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!