From ff71174d255eec515521468f54562f14da0d29ff Mon Sep 17 00:00:00 2001 From: Daniel Toyama Date: Wed, 22 Jul 2026 13:30:50 -0700 Subject: [PATCH] Internal change. PiperOrigin-RevId: 952300486 --- android_env/apps/README.md | 7 + .../AccessibilityForwarder.kt | 293 ---------- .../AccessibilityForwarderTest.kt | 516 ------------------ .../AccessibilityTreeCreator.kt | 235 -------- .../AccessibilityTreeCreatorTest.kt | 85 --- .../AndroidManifest.xml | 48 -- .../AndroidManifest_lite.xml | 24 - .../FlagsBroadcastReceiver.kt | 60 -- .../FlagsBroadcastReceiverTest.kt | 166 ------ .../accessibilityforwarder/LogFlags.kt | 32 -- .../ParentChildNodePair.kt | 40 -- .../UniqueIdsGenerator.kt | 29 - .../xml/accessibility_forwarder_service.xml | 22 - .../androidenv/catch/AndroidManifest.xml | 39 -- .../com/google/androidenv/catch/BUILD.bazel | 80 --- .../com/google/androidenv/catch/GameLogic.kt | 76 --- .../androidenv/catch/GameLogicThread.kt | 45 -- .../google/androidenv/catch/MainActivity.kt | 152 ------ .../google/androidenv/catch/RenderThread.kt | 53 -- .../androidenv/catch/res/layout/main.xml | 27 - .../androidenv/catch/res/values/strings.xml | 19 - .../androidenv/catch/sprite/BUILD.bazel | 64 --- .../androidenv/catch/sprite/Background.kt | 26 - .../google/androidenv/catch/sprite/Ball.kt | 108 ---- .../androidenv/catch/sprite/LineSegment.kt | 18 - .../google/androidenv/catch/sprite/Paddle.kt | 70 --- .../google/androidenv/catch/sprite/Point.kt | 18 - .../google/androidenv/catch/sprite/Sprite.kt | 24 - .../androidenv/catch/AndroidManifest.xml | 39 -- .../com/google/androidenv/catch/BUILD.bazel | 87 --- .../google/androidenv/catch/GameLogicTest.kt | 195 ------- .../androidenv/catch/GameLogicThreadTest.kt | 72 --- .../androidenv/catch/MainActivityTest.kt | 85 --- .../androidenv/catch/RenderThreadTest.kt | 136 ----- .../androidenv/catch/sprite/BUILD.bazel | 82 --- .../androidenv/catch/sprite/BackgroundTest.kt | 58 -- .../androidenv/catch/sprite/BallTest.kt | 387 ------------- .../androidenv/catch/sprite/PaddleTest.kt | 184 ------- .../androidenv/catch/sprite/SpriteTest.kt | 55 -- 39 files changed, 7 insertions(+), 3749 deletions(-) create mode 100644 android_env/apps/README.md delete mode 100644 android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityForwarder.kt delete mode 100644 android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityForwarderTest.kt delete mode 100644 android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityTreeCreator.kt delete mode 100644 android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityTreeCreatorTest.kt delete mode 100644 android_env/apps/java/com/google/androidenv/accessibilityforwarder/AndroidManifest.xml delete mode 100644 android_env/apps/java/com/google/androidenv/accessibilityforwarder/AndroidManifest_lite.xml delete mode 100644 android_env/apps/java/com/google/androidenv/accessibilityforwarder/FlagsBroadcastReceiver.kt delete mode 100644 android_env/apps/java/com/google/androidenv/accessibilityforwarder/FlagsBroadcastReceiverTest.kt delete mode 100644 android_env/apps/java/com/google/androidenv/accessibilityforwarder/LogFlags.kt delete mode 100644 android_env/apps/java/com/google/androidenv/accessibilityforwarder/ParentChildNodePair.kt delete mode 100644 android_env/apps/java/com/google/androidenv/accessibilityforwarder/UniqueIdsGenerator.kt delete mode 100644 android_env/apps/java/com/google/androidenv/accessibilityforwarder/res/xml/accessibility_forwarder_service.xml delete mode 100644 android_env/apps/java/com/google/androidenv/catch/AndroidManifest.xml delete mode 100644 android_env/apps/java/com/google/androidenv/catch/BUILD.bazel delete mode 100644 android_env/apps/java/com/google/androidenv/catch/GameLogic.kt delete mode 100644 android_env/apps/java/com/google/androidenv/catch/GameLogicThread.kt delete mode 100644 android_env/apps/java/com/google/androidenv/catch/MainActivity.kt delete mode 100644 android_env/apps/java/com/google/androidenv/catch/RenderThread.kt delete mode 100644 android_env/apps/java/com/google/androidenv/catch/res/layout/main.xml delete mode 100644 android_env/apps/java/com/google/androidenv/catch/res/values/strings.xml delete mode 100644 android_env/apps/java/com/google/androidenv/catch/sprite/BUILD.bazel delete mode 100644 android_env/apps/java/com/google/androidenv/catch/sprite/Background.kt delete mode 100644 android_env/apps/java/com/google/androidenv/catch/sprite/Ball.kt delete mode 100644 android_env/apps/java/com/google/androidenv/catch/sprite/LineSegment.kt delete mode 100644 android_env/apps/java/com/google/androidenv/catch/sprite/Paddle.kt delete mode 100644 android_env/apps/java/com/google/androidenv/catch/sprite/Point.kt delete mode 100644 android_env/apps/java/com/google/androidenv/catch/sprite/Sprite.kt delete mode 100644 android_env/apps/javatests/com/google/androidenv/catch/AndroidManifest.xml delete mode 100644 android_env/apps/javatests/com/google/androidenv/catch/BUILD.bazel delete mode 100644 android_env/apps/javatests/com/google/androidenv/catch/GameLogicTest.kt delete mode 100644 android_env/apps/javatests/com/google/androidenv/catch/GameLogicThreadTest.kt delete mode 100644 android_env/apps/javatests/com/google/androidenv/catch/MainActivityTest.kt delete mode 100644 android_env/apps/javatests/com/google/androidenv/catch/RenderThreadTest.kt delete mode 100644 android_env/apps/javatests/com/google/androidenv/catch/sprite/BUILD.bazel delete mode 100644 android_env/apps/javatests/com/google/androidenv/catch/sprite/BackgroundTest.kt delete mode 100644 android_env/apps/javatests/com/google/androidenv/catch/sprite/BallTest.kt delete mode 100644 android_env/apps/javatests/com/google/androidenv/catch/sprite/PaddleTest.kt delete mode 100644 android_env/apps/javatests/com/google/androidenv/catch/sprite/SpriteTest.kt diff --git a/android_env/apps/README.md b/android_env/apps/README.md new file mode 100644 index 00000000..92154ab8 --- /dev/null +++ b/android_env/apps/README.md @@ -0,0 +1,7 @@ +# Android Apps used by AndroidEnv. + + + +This folder contains libraries and Android apps that are used by AndroidEnv. +The apps here are "first-party" in the sense that they're maintained by the +AndroidEnv team. diff --git a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityForwarder.kt b/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityForwarder.kt deleted file mode 100644 index a2aba7e6..00000000 --- a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityForwarder.kt +++ /dev/null @@ -1,293 +0,0 @@ -// 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? = - try { - windows - } catch (e: NullPointerException) { - null - } - - /** Logs extras for all event types. */ - private fun logExtrasForEvent(event: AccessibilityEvent) { - - val events: MutableMap = 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() - 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() - } -} diff --git a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityForwarderTest.kt b/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityForwarderTest.kt deleted file mode 100644 index 4ad29367..00000000 --- a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityForwarderTest.kt +++ /dev/null @@ -1,516 +0,0 @@ -// 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? = 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. - } -} diff --git a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityTreeCreator.kt b/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityTreeCreator.kt deleted file mode 100644 index 87054118..00000000 --- a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityTreeCreator.kt +++ /dev/null @@ -1,235 +0,0 @@ -// 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): AndroidAccessibilityForest { - val sourcesMap: ConcurrentHashMap = - ConcurrentHashMap() - val windows: List = - processWindowsAndBlock(windowInfos, sourcesMap) - return androidAccessibilityForest { this.windows += windows } - } - - private fun processWindowsAndBlock( - windowInfos: List, - sourcesMap: ConcurrentHashMap, - ): List { - val windows: List - runBlocking { windows = processWindows(windowInfos, sourcesMap) } - return windows - } - - private suspend fun processWindows( - windowInfos: List, - sourcesMap: ConcurrentHashMap, - ): List { - var windowInfoProtos = mutableListOf() - 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, - ): 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 - 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, - ): AndroidAccessibilityTree { - Log.d(TAG, "processNodesInWindow()") - val traversalQueue = ArrayDeque() - traversalQueue.add(ParentChildNodePair.builder().child(root).build()) - val uniqueIdsCache: UniqueIdsGenerator = UniqueIdsGenerator() - var currentDepth = 0 - val nodesDeferred = mutableListOf>() - val seenNodes: HashSet = 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, - uniqueIdsCache: UniqueIdsGenerator, - 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, -): 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, -): List { - val ids = mutableListOf() - 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 - } diff --git a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityTreeCreatorTest.kt b/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityTreeCreatorTest.kt deleted file mode 100644 index 0184b568..00000000 --- a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AccessibilityTreeCreatorTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -// 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 - } -} diff --git a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AndroidManifest.xml b/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AndroidManifest.xml deleted file mode 100644 index 07e4a860..00000000 --- a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AndroidManifest.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AndroidManifest_lite.xml b/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AndroidManifest_lite.xml deleted file mode 100644 index 1d53cadc..00000000 --- a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/AndroidManifest_lite.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - diff --git a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/FlagsBroadcastReceiver.kt b/android_env/apps/java/com/google/androidenv/accessibilityforwarder/FlagsBroadcastReceiver.kt deleted file mode 100644 index 64530203..00000000 --- a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/FlagsBroadcastReceiver.kt +++ /dev/null @@ -1,60 +0,0 @@ -// 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" - } -} diff --git a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/FlagsBroadcastReceiverTest.kt b/android_env/apps/java/com/google/androidenv/accessibilityforwarder/FlagsBroadcastReceiverTest.kt deleted file mode 100644 index f1708f91..00000000 --- a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/FlagsBroadcastReceiverTest.kt +++ /dev/null @@ -1,166 +0,0 @@ -// 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) - } -} diff --git a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/LogFlags.kt b/android_env/apps/java/com/google/androidenv/accessibilityforwarder/LogFlags.kt deleted file mode 100644 index 6482c8d2..00000000 --- a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/LogFlags.kt +++ /dev/null @@ -1,32 +0,0 @@ -// 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 -} diff --git a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/ParentChildNodePair.kt b/android_env/apps/java/com/google/androidenv/accessibilityforwarder/ParentChildNodePair.kt deleted file mode 100644 index 7bc7ac6b..00000000 --- a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/ParentChildNodePair.kt +++ /dev/null @@ -1,40 +0,0 @@ -// 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() - } -} diff --git a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/UniqueIdsGenerator.kt b/android_env/apps/java/com/google/androidenv/accessibilityforwarder/UniqueIdsGenerator.kt deleted file mode 100644 index ee5609dd..00000000 --- a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/UniqueIdsGenerator.kt +++ /dev/null @@ -1,29 +0,0 @@ -// 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 { - private val nextId = AtomicInteger(0) - private val uniqueIdsByNode = ConcurrentHashMap() - - fun getUniqueId(a: A): Int { - return uniqueIdsByNode.computeIfAbsent(a, Function { _: A -> nextId.getAndIncrement() })!! - } -} diff --git a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/res/xml/accessibility_forwarder_service.xml b/android_env/apps/java/com/google/androidenv/accessibilityforwarder/res/xml/accessibility_forwarder_service.xml deleted file mode 100644 index 37643f8d..00000000 --- a/android_env/apps/java/com/google/androidenv/accessibilityforwarder/res/xml/accessibility_forwarder_service.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/android_env/apps/java/com/google/androidenv/catch/AndroidManifest.xml b/android_env/apps/java/com/google/androidenv/catch/AndroidManifest.xml deleted file mode 100644 index 95c55c9e..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/AndroidManifest.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/android_env/apps/java/com/google/androidenv/catch/BUILD.bazel b/android_env/apps/java/com/google/androidenv/catch/BUILD.bazel deleted file mode 100644 index da5fad41..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/BUILD.bazel +++ /dev/null @@ -1,80 +0,0 @@ -# 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", - ], -) diff --git a/android_env/apps/java/com/google/androidenv/catch/GameLogic.kt b/android_env/apps/java/com/google/androidenv/catch/GameLogic.kt deleted file mode 100644 index 78a1502b..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/GameLogic.kt +++ /dev/null @@ -1,76 +0,0 @@ -// 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) - } -} diff --git a/android_env/apps/java/com/google/androidenv/catch/GameLogicThread.kt b/android_env/apps/java/com/google/androidenv/catch/GameLogicThread.kt deleted file mode 100644 index f8baaa31..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/GameLogicThread.kt +++ /dev/null @@ -1,45 +0,0 @@ -// 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()}") - } - } -} diff --git a/android_env/apps/java/com/google/androidenv/catch/MainActivity.kt b/android_env/apps/java/com/google/androidenv/catch/MainActivity.kt deleted file mode 100644 index 0800cd05..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/MainActivity.kt +++ /dev/null @@ -1,152 +0,0 @@ -// 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" - } -} diff --git a/android_env/apps/java/com/google/androidenv/catch/RenderThread.kt b/android_env/apps/java/com/google/androidenv/catch/RenderThread.kt deleted file mode 100644 index d5d2e5ce..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/RenderThread.kt +++ /dev/null @@ -1,53 +0,0 @@ -// 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()) - } - } -} diff --git a/android_env/apps/java/com/google/androidenv/catch/res/layout/main.xml b/android_env/apps/java/com/google/androidenv/catch/res/layout/main.xml deleted file mode 100644 index e07f6682..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/res/layout/main.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - diff --git a/android_env/apps/java/com/google/androidenv/catch/res/values/strings.xml b/android_env/apps/java/com/google/androidenv/catch/res/values/strings.xml deleted file mode 100644 index 4550e0de..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/res/values/strings.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - Catch - diff --git a/android_env/apps/java/com/google/androidenv/catch/sprite/BUILD.bazel b/android_env/apps/java/com/google/androidenv/catch/sprite/BUILD.bazel deleted file mode 100644 index 11b4ff80..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/sprite/BUILD.bazel +++ /dev/null @@ -1,64 +0,0 @@ -# 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"], -) diff --git a/android_env/apps/java/com/google/androidenv/catch/sprite/Background.kt b/android_env/apps/java/com/google/androidenv/catch/sprite/Background.kt deleted file mode 100644 index fc7b0096..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/sprite/Background.kt +++ /dev/null @@ -1,26 +0,0 @@ -// 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) - } -} diff --git a/android_env/apps/java/com/google/androidenv/catch/sprite/Ball.kt b/android_env/apps/java/com/google/androidenv/catch/sprite/Ball.kt deleted file mode 100644 index 1247ebd6..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/sprite/Ball.kt +++ /dev/null @@ -1,108 +0,0 @@ -// 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) - } -} diff --git a/android_env/apps/java/com/google/androidenv/catch/sprite/LineSegment.kt b/android_env/apps/java/com/google/androidenv/catch/sprite/LineSegment.kt deleted file mode 100644 index 56d37a6d..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/sprite/LineSegment.kt +++ /dev/null @@ -1,18 +0,0 @@ -// 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) diff --git a/android_env/apps/java/com/google/androidenv/catch/sprite/Paddle.kt b/android_env/apps/java/com/google/androidenv/catch/sprite/Paddle.kt deleted file mode 100644 index a7276adc..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/sprite/Paddle.kt +++ /dev/null @@ -1,70 +0,0 @@ -// 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) - } -} diff --git a/android_env/apps/java/com/google/androidenv/catch/sprite/Point.kt b/android_env/apps/java/com/google/androidenv/catch/sprite/Point.kt deleted file mode 100644 index 4e2e87eb..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/sprite/Point.kt +++ /dev/null @@ -1,18 +0,0 @@ -// 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) diff --git a/android_env/apps/java/com/google/androidenv/catch/sprite/Sprite.kt b/android_env/apps/java/com/google/androidenv/catch/sprite/Sprite.kt deleted file mode 100644 index ed6ccc41..00000000 --- a/android_env/apps/java/com/google/androidenv/catch/sprite/Sprite.kt +++ /dev/null @@ -1,24 +0,0 @@ -// 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) {} -} diff --git a/android_env/apps/javatests/com/google/androidenv/catch/AndroidManifest.xml b/android_env/apps/javatests/com/google/androidenv/catch/AndroidManifest.xml deleted file mode 100644 index 95c55c9e..00000000 --- a/android_env/apps/javatests/com/google/androidenv/catch/AndroidManifest.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/android_env/apps/javatests/com/google/androidenv/catch/BUILD.bazel b/android_env/apps/javatests/com/google/androidenv/catch/BUILD.bazel deleted file mode 100644 index 8862f3e9..00000000 --- a/android_env/apps/javatests/com/google/androidenv/catch/BUILD.bazel +++ /dev/null @@ -1,87 +0,0 @@ -# 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", - ], -) diff --git a/android_env/apps/javatests/com/google/androidenv/catch/GameLogicTest.kt b/android_env/apps/javatests/com/google/androidenv/catch/GameLogicTest.kt deleted file mode 100644 index b4e7d1d6..00000000 --- a/android_env/apps/javatests/com/google/androidenv/catch/GameLogicTest.kt +++ /dev/null @@ -1,195 +0,0 @@ -// 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) - } -} diff --git a/android_env/apps/javatests/com/google/androidenv/catch/GameLogicThreadTest.kt b/android_env/apps/javatests/com/google/androidenv/catch/GameLogicThreadTest.kt deleted file mode 100644 index a2ea78c9..00000000 --- a/android_env/apps/javatests/com/google/androidenv/catch/GameLogicThreadTest.kt +++ /dev/null @@ -1,72 +0,0 @@ -// 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() - } -} diff --git a/android_env/apps/javatests/com/google/androidenv/catch/MainActivityTest.kt b/android_env/apps/javatests/com/google/androidenv/catch/MainActivityTest.kt deleted file mode 100644 index d064b3c9..00000000 --- a/android_env/apps/javatests/com/google/androidenv/catch/MainActivityTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -// 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(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" - } -} diff --git a/android_env/apps/javatests/com/google/androidenv/catch/RenderThreadTest.kt b/android_env/apps/javatests/com/google/androidenv/catch/RenderThreadTest.kt deleted file mode 100644 index a65d8fb8..00000000 --- a/android_env/apps/javatests/com/google/androidenv/catch/RenderThreadTest.kt +++ /dev/null @@ -1,136 +0,0 @@ -// 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) - } -} diff --git a/android_env/apps/javatests/com/google/androidenv/catch/sprite/BUILD.bazel b/android_env/apps/javatests/com/google/androidenv/catch/sprite/BUILD.bazel deleted file mode 100644 index 37939ecb..00000000 --- a/android_env/apps/javatests/com/google/androidenv/catch/sprite/BUILD.bazel +++ /dev/null @@ -1,82 +0,0 @@ -# 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", - ], -) diff --git a/android_env/apps/javatests/com/google/androidenv/catch/sprite/BackgroundTest.kt b/android_env/apps/javatests/com/google/androidenv/catch/sprite/BackgroundTest.kt deleted file mode 100644 index 6341f492..00000000 --- a/android_env/apps/javatests/com/google/androidenv/catch/sprite/BackgroundTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -// 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) - } -} diff --git a/android_env/apps/javatests/com/google/androidenv/catch/sprite/BallTest.kt b/android_env/apps/javatests/com/google/androidenv/catch/sprite/BallTest.kt deleted file mode 100644 index 3f96d923..00000000 --- a/android_env/apps/javatests/com/google/androidenv/catch/sprite/BallTest.kt +++ /dev/null @@ -1,387 +0,0 @@ -// 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() - 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. - ) - } - } -} diff --git a/android_env/apps/javatests/com/google/androidenv/catch/sprite/PaddleTest.kt b/android_env/apps/javatests/com/google/androidenv/catch/sprite/PaddleTest.kt deleted file mode 100644 index 81f4e143..00000000 --- a/android_env/apps/javatests/com/google/androidenv/catch/sprite/PaddleTest.kt +++ /dev/null @@ -1,184 +0,0 @@ -// 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() - val paintCaptor = argumentCaptor() - 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() - val paintCaptor = argumentCaptor() - 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) - } - } - } - } -} diff --git a/android_env/apps/javatests/com/google/androidenv/catch/sprite/SpriteTest.kt b/android_env/apps/javatests/com/google/androidenv/catch/sprite/SpriteTest.kt deleted file mode 100644 index 8eb846b1..00000000 --- a/android_env/apps/javatests/com/google/androidenv/catch/sprite/SpriteTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -// 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) - } -}