From ebd27e15d6e3627600272cc99ddfba028d5b586c Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Mon, 6 May 2024 15:59:36 +0100 Subject: [PATCH 1/2] POC of a Roborazzi Emulator --- emulator-client/.gitignore | 1 + emulator-client/build.gradle.kts | 38 + .../roborazzi/emulator/ClientMain.kt | 48 + emulator-proto/.gitignore | 1 + emulator-proto/build.gradle.kts | 24 + .../roborazzi/emulator/GrpcMetadata.kt | 10 + .../src/main/proto/emulator_controller.proto | 1376 +++++++++++++++++ emulator-proto/src/main/proto/roborazzi.proto | 38 + .../src/main/proto/rtc_service.proto | 117 ++ emulator-server/.gitignore | 1 + emulator-server/build.gradle.kts | 99 ++ emulator-server/src/main/AndroidManifest.xml | 9 + .../emulator/EmulatorControllerService.kt | 152 ++ .../roborazzi/emulator/RoboInstances.kt | 25 + .../roborazzi/emulator/RoborazziService.kt | 42 + .../roborazzi/emulator/RtcService.kt | 6 + .../emulator/previews/SimplePreview.kt | 29 + .../roborazzi/emulator/GrpcServer.kt | 57 + .../takahirom/roborazzi/emulator/Main.kt | 38 + .../emulator/RoboEmulatorServerTest.kt | 42 + .../takahirom/roborazzi/emulator/RoboEnv.kt | 95 ++ gradle.properties | 82 +- gradle/libs.versions.toml | 28 +- settings.gradle | 4 + 24 files changed, 2308 insertions(+), 54 deletions(-) create mode 100644 emulator-client/.gitignore create mode 100644 emulator-client/build.gradle.kts create mode 100644 emulator-client/src/test/kotlin/com/github/takahirom/roborazzi/emulator/ClientMain.kt create mode 100644 emulator-proto/.gitignore create mode 100644 emulator-proto/build.gradle.kts create mode 100644 emulator-proto/src/main/kotlin/com/github/takahirom/roborazzi/emulator/GrpcMetadata.kt create mode 100644 emulator-proto/src/main/proto/emulator_controller.proto create mode 100644 emulator-proto/src/main/proto/roborazzi.proto create mode 100644 emulator-proto/src/main/proto/rtc_service.proto create mode 100644 emulator-server/.gitignore create mode 100644 emulator-server/build.gradle.kts create mode 100644 emulator-server/src/main/AndroidManifest.xml create mode 100644 emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/EmulatorControllerService.kt create mode 100644 emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/RoboInstances.kt create mode 100644 emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/RoborazziService.kt create mode 100644 emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/RtcService.kt create mode 100644 emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/previews/SimplePreview.kt create mode 100644 emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/GrpcServer.kt create mode 100644 emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/Main.kt create mode 100644 emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/RoboEmulatorServerTest.kt create mode 100644 emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/RoboEnv.kt diff --git a/emulator-client/.gitignore b/emulator-client/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/emulator-client/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/emulator-client/build.gradle.kts b/emulator-client/build.gradle.kts new file mode 100644 index 00000000..9d9d5167 --- /dev/null +++ b/emulator-client/build.gradle.kts @@ -0,0 +1,38 @@ +@file:Suppress("UnstableApiUsage") + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") + id("com.squareup.wire") version "4.9.9" +} + +group = "com.github.takahirom.roborazzi.emulator.client" +version = "1.0-SNAPSHOT" + +dependencies { + api(libs.grpc.protobuf) + implementation(libs.grpc.kotlin.stub) + implementation(libs.wire.grpc.client) + + implementation(project(":emulator-proto")) + protoPath(project(":emulator-proto")) +} + +wire { + sourcePath { + srcProject(":emulator-proto") + } + + kotlin { + rpcRole = "client" + singleMethodServices = false + rpcCallStyle = "suspending" + } +} + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs += "-Xcontext-receivers" + } +} \ No newline at end of file diff --git a/emulator-client/src/test/kotlin/com/github/takahirom/roborazzi/emulator/ClientMain.kt b/emulator-client/src/test/kotlin/com/github/takahirom/roborazzi/emulator/ClientMain.kt new file mode 100644 index 00000000..787f1c1b --- /dev/null +++ b/emulator-client/src/test/kotlin/com/github/takahirom/roborazzi/emulator/ClientMain.kt @@ -0,0 +1,48 @@ +package com.github.takahirom.roborazzi.emulator + +import com.android.emulator.control.EmulatorControllerClient +import com.android.emulator.control.ImageFormat +import com.squareup.wire.GrpcClient +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okio.FileSystem +import okio.Path.Companion.toPath +import roborazzi.emulator.ComposePreview +import roborazzi.emulator.RoborazziClient + +suspend fun main() { + val port = 8080 + val grpcClient = GrpcClient.Builder() + .client( + OkHttpClient.Builder() + .protocols(listOf(Protocol.H2_PRIOR_KNOWLEDGE)) + .build()) + .baseUrl("http://localhost:$port") + .build() + + val roborazziService = grpcClient.create(RoborazziClient::class) + val emulatorControllerClient = grpcClient.create(EmulatorControllerClient::class) + + val metadata = mapOf("instance" to "1") + + println("--> getGps") + val gpsCall = emulatorControllerClient.getGps() + gpsCall.requestMetadata = metadata + println(gpsCall.execute(Unit)) + + println("--> launchComposePreview") + val previewCall = roborazziService.launchComposePreview() + previewCall.requestMetadata = metadata + previewCall.execute(ComposePreview(previewMethod = "com.github.takahirom.roborazzi.emulator.previews.SimplePreviewKt.SimplePreview")) + + println("--> getScreenshot") + val screenshotCall = emulatorControllerClient.getScreenshot() + screenshotCall.requestMetadata = metadata + val image = screenshotCall.execute(ImageFormat()) + println(image) + val path = "emulator-client/build/SimplePreview.png".toPath() + FileSystem.SYSTEM.write(path) { + write(image.image) + } + println("Wrote: $path") +} \ No newline at end of file diff --git a/emulator-proto/.gitignore b/emulator-proto/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/emulator-proto/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/emulator-proto/build.gradle.kts b/emulator-proto/build.gradle.kts new file mode 100644 index 00000000..de5794be --- /dev/null +++ b/emulator-proto/build.gradle.kts @@ -0,0 +1,24 @@ +@file:Suppress("UnstableApiUsage") + +plugins { + kotlin("jvm") + id("com.squareup.wire") version "4.9.9" +} + +group = "com.github.takahirom.roborazzi.emulator.proto" +version = "1.0-SNAPSHOT" + +dependencies { + api(libs.grpc.protobuf) + api(libs.wire.grpc.client) +} + +wire { + protoLibrary = true + +// kotlin { +// rpcCallStyle = "suspending" +// rpcRole = "client" +// singleMethodServices = false +// } +} diff --git a/emulator-proto/src/main/kotlin/com/github/takahirom/roborazzi/emulator/GrpcMetadata.kt b/emulator-proto/src/main/kotlin/com/github/takahirom/roborazzi/emulator/GrpcMetadata.kt new file mode 100644 index 00000000..5e5a3a3b --- /dev/null +++ b/emulator-proto/src/main/kotlin/com/github/takahirom/roborazzi/emulator/GrpcMetadata.kt @@ -0,0 +1,10 @@ +package com.github.takahirom.roborazzi.emulator + +import io.grpc.Context +import io.grpc.Metadata +import io.grpc.Metadata.ASCII_STRING_MARSHALLER + +object GrpcMetadata { + val instanceMetadataKey = Metadata.Key.of("instance", ASCII_STRING_MARSHALLER) + val instanceContextKey = Context.key("instance") +} \ No newline at end of file diff --git a/emulator-proto/src/main/proto/emulator_controller.proto b/emulator-proto/src/main/proto/emulator_controller.proto new file mode 100644 index 00000000..17ff7b23 --- /dev/null +++ b/emulator-proto/src/main/proto/emulator_controller.proto @@ -0,0 +1,1376 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// 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. + +// Note that if you add/remove methods in this file you must update +// the metrics sql as well ./android/scripts/gen-grpc-sql.py +// +// Please group deleted methods in a block including the date (MM/DD/YY) +// it was removed. This enables us to easily keep metrics around after removal +// +// List of deleted methods +// rpc iWasDeleted (03/12/12) +// ... +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "com.android.emulator.control"; +option objc_class_prefix = "AEC"; + +package android.emulation.control; +import "google/protobuf/empty.proto"; + +// An EmulatorController service lets you control the emulator. +// Note that this is currently an experimental feature, and that the +// service definition might change without notice. Use at your own risk! +// +// We use the following rough conventions: +// +// streamXXX --> streams values XXX (usually for emulator lifetime). Values +// are updated as soon as they become available. +// getXXX --> gets a single value XXX +// setXXX --> sets a single value XXX, does not returning state, these +// usually have an observable lasting side effect. +// sendXXX --> send a single event XXX, possibly returning state information. +// android usually responds to these events. +service EmulatorController { + // set/get/stream the sensor data + rpc streamSensor(SensorValue) returns (stream SensorValue) {} + rpc getSensor(SensorValue) returns (SensorValue) {} + rpc setSensor(SensorValue) returns (google.protobuf.Empty) {} + + // set/get/stream the physical model, this is likely the one you are + // looking for when you wish to modify the device state. + rpc setPhysicalModel(PhysicalModelValue) returns (google.protobuf.Empty) {} + rpc getPhysicalModel(PhysicalModelValue) returns (PhysicalModelValue) {} + rpc streamPhysicalModel(PhysicalModelValue) + returns (stream PhysicalModelValue) {} + + // Atomically set/get the current primary clipboard data. + // Note that a call to setClipboard will result in an immediate + // event for those who made a call to streamClipboard and are + // on a different channel than the one used to set the clipboard. + rpc setClipboard(ClipData) returns (google.protobuf.Empty) {} + rpc getClipboard(google.protobuf.Empty) returns (ClipData) {} + + // Streams the current data on the clipboard. This will immediately produce + // a result with the current state of the clipboard after which the stream + // will block and wait until a new clip event is available from the guest. + // Calling the setClipboard method above will not result in generating a + // clip event. It is possible to lose clipboard events if the clipboard + // updates very rapidly. + rpc streamClipboard(google.protobuf.Empty) returns (stream ClipData) {} + + // Set/get the battery to the given state. + rpc setBattery(BatteryState) returns (google.protobuf.Empty) {} + rpc getBattery(google.protobuf.Empty) returns (BatteryState) {} + + // Set the state of the gps. + // Note: Setting the gps position will not be reflected in the user + // interface. Keep in mind that android usually only samples the gps at 1 + // hz. + rpc setGps(GpsState) returns (google.protobuf.Empty) {} + + // Gets the latest gps state as delivered by the setGps call, or location ui + // if active. + // + // Note: this is not necessarily the actual gps coordinate visible at the + // time, due to gps sample frequency (usually 1hz). + rpc getGps(google.protobuf.Empty) returns (GpsState) {} + + // Simulate a touch event on the finger print sensor. + rpc sendFingerprint(Fingerprint) returns (google.protobuf.Empty) {} + + // Send a keyboard event. Translating the event. + rpc sendKey(KeyboardEvent) returns (google.protobuf.Empty) {} + // Send touch/mouse events. Note that mouse events can be simulated + // by touch events. + rpc sendTouch(TouchEvent) returns (google.protobuf.Empty) {} + rpc sendMouse(MouseEvent) returns (google.protobuf.Empty) {} + rpc injectWheel(stream WheelEvent) returns (google.protobuf.Empty) {} + + // Stream a series of input events to the emulator, the events will + // arrive in order. + rpc streamInputEvent(stream InputEvent) returns (google.protobuf.Empty) {} + + // Make a phone call. + rpc sendPhone(PhoneCall) returns (PhoneResponse) {} + + // Sends an sms message to the emulator. + rpc sendSms(SmsMessage) returns (PhoneResponse) {} + + // Sends an sms message to the emulator. + rpc setPhoneNumber(PhoneNumber) returns (PhoneResponse) {} + + // Retrieve the status of the emulator. This will contain general + // hardware information, and whether the device has booted or not. + rpc getStatus(google.protobuf.Empty) returns (EmulatorStatus) {} + + // Gets an individual screenshot in the desired format. + // + // The image will be scaled to the desired ImageFormat, while maintaining + // the aspect ratio. The returned image will never exceed resolution of the + // device display. Not setting the width or height (i.e. they are 0) will + // result in using the display width and height. + // + // The resulting image will be properly oriented and can be displayed + // directly without post processing. For example, if the device has a + // 1080x1920 screen and is in landscape mode and called with no width or + // height parameter, it will return a 1920x1080 image. + // + // The dimensions of the returned image will never exceed the corresponding + // display dimensions. For example, this method will return a 1920x1080 + // screenshot, if the display resolution is 1080x1920 and a screenshot of + // 2048x2048 is requested when the device is in landscape mode. + // + // This method will return an empty image if the display is not visible. + rpc getScreenshot(ImageFormat) returns (Image) {} + + // Streams a series of screenshots in the desired format. + // + // A new frame will be delivered whenever the device produces a new frame. + // Beware that this can produce a significant amount of data and that + // certain translations can be very costly. For example, streaming a series + // of png images is very cpu intensive. + // + // Images are produced according to the getScreenshot API described above. + // + // If the display is inactive, or becomes inactive, an empty image will be + // delivered. Images will be delived again if the display becomes active and + // new frames are produced. + rpc streamScreenshot(ImageFormat) returns (stream Image) {} + + // Streams a series of audio packets in the desired format. + // A new frame will be delivered whenever the emulated device + // produces a new audio frame. You can expect packets to be + // delivered in intervals of 20-30ms. + // + // Be aware that this can block when the emulator does not + // produce any audio whatsoever! + rpc streamAudio(AudioFormat) returns (stream AudioPacket) {} + + // Injects a series of audio packets to the android microphone. + // A new frame will be delivered whenever the emulated device + // requests a new audio frame. Audio is usually delivered at a rate + // that the emulator is requesting frames. Audio will be stored in a + // temporary buffer that can hold 300ms of audio. + // + // Notes: + // - Only the first audio format packet that is delivered will be + // honored. There is no need to send the audio format multiple times. + // - Real time audio currently immediately overrides the buffer. This + // means you must provide a constant rate of audio packets. The real + // time mode is experimental. Timestamps of audio packets might be + // used in the future to improve synchronization. + // + // - INVALID_ARGUMENT (code 3) The sampling rate was too high/low + // - INVALID_ARGUMENT (code 3) The audio packet was too large to handle. + // - FAILED_PRECONDITION (code 9) If there was a microphone registered + // already. + rpc injectAudio(stream AudioPacket) returns (google.protobuf.Empty) {} + + // Deprecated, please use the streamLogcat method instead. + rpc getLogcat(LogMessage) returns (LogMessage) { + option deprecated = true; + } + + // Streams the logcat output from the emulator. + // Note that parsed logcat messages are only available after L (Api >23) + rpc streamLogcat(LogMessage) returns (stream LogMessage) {} + + // Transition the virtual machine to the desired state. Note that + // some states are only observable. For example you cannot transition + // to the error state. + rpc setVmState(VmRunState) returns (google.protobuf.Empty) {} + + // Gets the state of the virtual machine. + rpc getVmState(google.protobuf.Empty) returns (VmRunState) {} + + // Atomically changes the current multi-display configuration. + // After this call the given display configurations will be activated. You + // can only update secondary displays. Displays with id 0 will be ignored. + // + // This call can result in the removal or addition of secondary displays, + // the final display state can be observed by the returned configuration. + // + // The following gRPC error codes can be returned: + // - FAILED_PRECONDITION (code 9) if the AVD does not support a + // configurable + // secondary display. + // - INVALID_ARGUMENT (code 3) if: + // - The same display id is defined multiple times. + // - The display configurations are outside valid ranges. + // See DisplayConfiguration for details on valid ranges. + // - INTERNAL (code 13) if there was an internal emulator failure. + rpc setDisplayConfigurations(DisplayConfigurations) + returns (DisplayConfigurations) {} + + // Returns all currently valid logical displays. + // + // The gRPC error code FAILED_PRECONDITION (code 9) is returned if the AVD + // does not support a configurable secondary display. + rpc getDisplayConfigurations(google.protobuf.Empty) + returns (DisplayConfigurations) {} + + // Notifies client of the following changes: + // + // - Virtual scene camera status change. + // - Display configuration changes from extended ui. This will only be fired + // if the user makes modifications the extended displays through the + // extended control tab. + // + // Note that this method will send the initial virtual scene state + // immediately. + rpc streamNotification(google.protobuf.Empty) + returns (stream Notification) {} + + // RotationRadian is relative to the camera's current orientation. + rpc rotateVirtualSceneCamera(RotationRadian) + returns (google.protobuf.Empty) {} + // Velocity is absolute + rpc setVirtualSceneCameraVelocity(Velocity) + returns (google.protobuf.Empty) {} + // Set foldable posture + rpc setPosture(Posture) returns (google.protobuf.Empty) {} + + // Get the backlight brightness. + // The following gRPC error codes can be returned: + // - FAILED_PRECONDITION (code 9) if the AVD does not support hw-control. + rpc getBrightness(BrightnessValue) returns (BrightnessValue) {} + + // Set the backlight brightness. + // The following gRPC error codes can be returned: + // - FAILED_PRECONDITION (code 9) if the AVD does not support hw-control. + // - INVALID_ARGUMENT (code 3) The brightness exceeds the valid range. + rpc setBrightness(BrightnessValue) returns (google.protobuf.Empty) {} + + // Returns the current mode of the primary display of a resizable AVD. + // The following gRPC error codes can be returned: + // - FAILED_PRECONDITION (code 9) if the AVD is not resizable. + rpc getDisplayMode(google.protobuf.Empty) returns (DisplayMode) {} + + // Sets the size of the primary display of a resizable AVD. Fails if the AVD + // is not resizable. The following gRPC error codes can be returned: + // - FAILED_PRECONDITION (code 9) if the AVD is not resizable. + rpc setDisplayMode(DisplayMode) returns (google.protobuf.Empty) {} +} + +// A Run State that describes the state of the Virtual Machine. +message VmRunState { + enum RunState { + // The emulator is in an unknown state. You cannot transition to this + // state. + UNKNOWN = 0; + // Guest is actively running. You can transition to this state from the + // paused state. + RUNNING = 1; + // Guest is paused to load a snapshot. You cannot transition to this + // state. + RESTORE_VM = 2; + // Guest has been paused. Transitioning to this state will pause the + // emulator the guest will not be consuming any cpu cycles. + PAUSED = 3; + // Guest is paused to take or export a snapshot. You cannot + // transition to this state. + SAVE_VM = 4; + // System shutdown, note that it is similar to power off. It tries to + // set the system status and notify guest. The system is likely going to + // disappear soon and do proper cleanup of resources, possibly taking + // a snapshot. This is the same behavior as closing the emulator by + // clicking the X (close) in the user interface. + SHUTDOWN = 5; + // Immediately terminate the emulator. No resource cleanup will take + // place. There is a good change to corrupt the system. + TERMINATE = 7; + // Will cause the emulator to reset. This is not a state you can + // observe. + RESET = 9; + // Guest experienced some error state, you cannot transition to this + // state. + INTERNAL_ERROR = 10; + // Completely restart the emulator. + RESTART = 11; + // Resume a stopped emulator + START = 12; + // Stop (pause) a running emulator + STOP = 13; + } + + RunState state = 1; +} + +message ParameterValue { + repeated float data = 1 [packed = true]; +} + +message PhysicalModelValue { + enum State { + OK = 0; + NO_SERVICE = -3; // qemud service is not available/initiated. + DISABLED = -2; // Sensor is disabled. + UNKNOWN = -1; // Unknown sensor (should not happen) + } + + // Details on the sensors documentation can be found here: + // https://developer.android.com/reference/android/hardware/Sensor.html#TYPE_ + // The types must follow the order defined in + // "external/qemu/android/hw-sensors.h" + enum PhysicalType { + POSITION = 0; + + // All values are angles in degrees. + // values = [x,y,z] + ROTATION = 1; + + MAGNETIC_FIELD = 2; + + // Temperature in °C + TEMPERATURE = 3; + + // Proximity sensor distance measured in centimeters + PROXIMITY = 4; + + // Ambient light level in SI lux units + LIGHT = 5; + + // Atmospheric pressure in hPa (millibar) + PRESSURE = 6; + + // Relative ambient air humidity in percent + HUMIDITY = 7; + + VELOCITY = 8; + AMBIENT_MOTION = 9; + + // Describing a hinge angle sensor in degrees. + HINGE_ANGLE0 = 10; + HINGE_ANGLE1 = 11; + HINGE_ANGLE2 = 12; + + ROLLABLE0 = 13; + ROLLABLE1 = 14; + ROLLABLE2 = 15; + + // Describing the device posture; the value should be an enum defined + // in Posture::PostureValue. + POSTURE = 16; + + // Heart rate in bpm + HEART_RATE = 17; + + // Ambient RGBC light intensity. Values are in order (Red, Green, Blue, + // Clear). + RGBC_LIGHT = 18; + + // Wrist tilt gesture (1 = gaze, 0 = ungaze) + WRIST_TILT = 19; + } + PhysicalType target = 1; + + // [Output Only] + State status = 2; + + // Value interpretation depends on sensor. + ParameterValue value = 3; +} + +// A single sensor value. +message SensorValue { + enum State { + OK = 0; + NO_SERVICE = -3; // qemud service is not available/initiated. + DISABLED = -2; // Sensor is disabled. + UNKNOWN = -1; // Unknown sensor (should not happen) + } + + // These are the various sensors that can be available in an emulated + // devices. + enum SensorType { + // Measures the acceleration force in m/s2 that is applied to a device + // on all three physical axes (x, y, and z), including the force of + // gravity. + ACCELERATION = 0; + // Measures a device's rate of rotation in rad/s around each of the + // three physical axes (x, y, and z). + GYROSCOPE = 1; + // Measures the ambient geomagnetic field for all three physical axes + // (x, y, z) in μT. + MAGNETIC_FIELD = 2; + // Measures degrees of rotation that a device makes around all three + // physical axes (x, y, z) + ORIENTATION = 3; + // Measures the temperature of the device in degrees Celsius (°C). + TEMPERATURE = 4; + // Measures the proximity of an object in cm relative to the view screen + // of a device. This sensor is typically used to determine whether a + // handset is being held up to a person's ear. + PROXIMITY = 5; + // Measures the ambient light level (illumination) in lx. + LIGHT = 6; + // Measures the ambient air pressure in hPa or mbar. + PRESSURE = 7; + // Measures the relative ambient humidity in percent (%). + HUMIDITY = 8; + MAGNETIC_FIELD_UNCALIBRATED = 9; + GYROSCOPE_UNCALIBRATED = 10; + + // HINGE_ANGLE0 (11), HINGE_ANGLE1 (12), HINGE_ANGLE2 (13) are + // skipped; clients should use get/setPhysicalModel() instead for these + // "sensors". + + // Measures the heart rate in bpm. + HEART_RATE = 14; + // Measures the ambient RGBC light intensity. + // Values are in order (Red, Green, Blue, Clear). + RGBC_LIGHT = 15; + // WIRST_TILT (16) is skipped; clients should use get/setPhysicalModel() + // instead. + // Measures acceleration force and provides bias data. + ACCELERATION_UNCALIBRATED = 17; + } + + // Type of sensor + SensorType target = 1; + + // [Output Only] + State status = 2; + + // Value interpretation depends on sensor enum. + ParameterValue value = 3; +} + +// A single backlight brightness value. +message BrightnessValue { + enum LightType { + // Display backlight. This will affect all displays. + LCD = 0; + KEYBOARD = 1; + BUTTON = 2; + } + + // Type of light + LightType target = 1; + + // Light intensity, ranges from 0-255. + uint32 value = 2; +} + +// in line with android/emulation/resizable_display_config.h +enum DisplayModeValue { + PHONE = 0; + FOLDABLE = 1; + TABLET = 2; + DESKTOP = 3; +} + +message DisplayMode { + DisplayModeValue value = 1; +} + +message LogMessage { + // [Output Only] The contents of the log output. + string contents = 1; + // The starting byte position of the output that was returned. This + // should match the start parameter sent with the request. If the serial + // console output exceeds the size of the buffer, older output will be + // overwritten by newer content and the start values will be mismatched. + int64 start = 2 [deprecated = true]; + //[Output Only] The position of the next byte of content from the serial + // console output. Use this value in the next request as the start + // parameter. + int64 next = 3 [deprecated = true]; + + // Set the sort of response you are interested it in. + // It the type is "Parsed" the entries field will contain the parsed + // results. otherwise the contents field will be set. + LogType sort = 4; + + // [Output Only] The parsed logcat entries so far. Only set if sort is + // set to Parsed + repeated LogcatEntry entries = 5; + + enum LogType { + Text = 0; + Parsed = 1; + } +} + +// A parsed logcat entry. +message LogcatEntry { + // The possible log levels. + enum LogLevel { + UNKNOWN = 0; + DEFAULT = 1; + VERBOSE = 2; + DEBUG = 3; + INFO = 4; + WARN = 5; + ERR = 6; + FATAL = 7; + SILENT = 8; + } + + // A Unix timestamps in milliseconds (The number of milliseconds that + // have elapsed since January 1, 1970 (midnight UTC/GMT), not counting + // leap seconds) + uint64 timestamp = 1; + + // Process id. + uint32 pid = 2; + + // Thread id. + uint32 tid = 3; + LogLevel level = 4; + string tag = 5; + string msg = 6; +} + +// Information about the hypervisor that is currently in use. +message VmConfiguration { + enum VmHypervisorType { + // An unknown hypervisor + UNKNOWN = 0; + + // No hypervisor is in use. This usually means that the guest is + // running on a different CPU than the host, or you are using a + // platform where no hypervisor is available. + NONE = 1; + + // The Kernel based Virtual Machine + // (https://www.linux-kvm.org/page/Main_Page) + KVM = 2; + + // Intel® Hardware Accelerated Execution Manager (Intel® HAXM) + // https://github.com/intel/haxm + HAXM = 3; + + // Hypervisor Framework. + // https://developer.apple.com/documentation/hypervisor + HVF = 4; + + // Window Hypervisor Platform + // https://docs.microsoft.com/en-us/virtualization/api/ + WHPX = 5; + + AEHD = 6; + } + + VmHypervisorType hypervisorType = 1; + int32 numberOfCpuCores = 2; + int64 ramSizeBytes = 3; +} + +// Representation of a clipped data object on the clipboard. +message ClipData { + // UTF-8 Encoded text. + string text = 1; +} + +// The Touch interface represents a single contact point on a +// touch-sensitive device. The contact point is commonly a finger or stylus +// and the device may be a touchscreen or trackpad. +message Touch { + // The horizontal coordinate. This is the physical location on the + // screen For example 0 indicates the leftmost coordinate. + int32 x = 1; + + // The vertical coordinate. This is the physical location on the screen + // For example 0 indicates the top left coordinate. + int32 y = 2; + + // The identifier is an arbitrary non-negative integer that is used to + // identify and track each tool independently when multiple tools are + // active. For example, when multiple fingers are touching the device, + // each finger should be assigned a distinct tracking id that is used as + // long as the finger remains in contact. Tracking ids may be reused + // when their associated tools move out of range. + // + // The emulator currently supports up to 10 concurrent touch events. The + // identifier can be any uninque value and will be mapped to the next + // available internal identifier. + int32 identifier = 3; + + // Reports the physical pressure applied to the tip of the tool or the + // signal strength of the touch contact. + // + // The values reported must be non-zero when the tool is touching the + // device and zero otherwise to indicate that the touch event is + // completed. + // + // Make sure to deliver a pressure of 0 for the given identifier when + // the touch event is completed, otherwise the touch identifier will not + // be unregistered! + int32 pressure = 4; + + // Optionally reports the cross-sectional area of the touch contact, or + // the length of the longer dimension of the touch contact. + int32 touch_major = 5; + + // Optionally reports the length of the shorter dimension of the touch + // contact. This axis will be ignored if touch_major is reporting an + // area measurement greater than 0. + int32 touch_minor = 6; + + enum EventExpiration { + // The system will use the default time of 120s to track + // the touch event with the given identifier. If no update happens + // within this timeframe the identifier is considered expired + // and can be made available for re-use. This means that a touch event + // with pressure 0 for this identifier will be send to the emulator. + EVENT_EXPIRATION_UNSPECIFIED = 0; + + // Never expire the given slot. You must *ALWAYS* close the identifier + // by sending a touch event with 0 pressure. + NEVER_EXPIRE = 1; + } + + EventExpiration expiration = 7; + + // The orientation of the contact, if any. + int32 orientation = 8; +} + +// A Pen is similar to a touch, with the addition +// of button and rubber information. +message Pen { + Touch location = 1; + + // True if the button is pressed or not + bool button_pressed = 2; + + // True if it is a rubber pointer. + bool rubber_pointer = 3; +} + +// A TouchEvent contains a list of Touch objects that are in contact with +// the touch surface. +// +// Touch events are delivered in sequence as specified in the touchList. +// +// TouchEvents are delivered to the emulated devices using ["Protocol +// B"](https://www.kernel.org/doc/Documentation/input/multi-touch-protocol.txt) +message TouchEvent { + // The list of Touch objects, note that these do not need to be unique + repeated Touch touches = 1; + + // The display device where the touch event occurred. + // Omitting or using the value 0 indicates the main display. + int32 display = 2; +} + +message PenEvent { + // The list of Pen objects, note that these do not need to be unique + repeated Pen events = 1; + + // The display device where the pen event occurred. + // Omitting or using the value 0 indicates the main display. + int32 display = 2; +} + +// The MouseEvent interface represents events that occur due to the user +// interacting with a pointing device (such as a mouse). +message MouseEvent { + // The horizontal coordinate. This is the physical location on the + // screen For example 0 indicates the leftmost coordinate. + int32 x = 1; + + // The vertical coordinate. This is the physical location on the screen + // For example 0 indicates the top left coordinate. + int32 y = 2; + + // Indicates which buttons are pressed. + // 0: No button was pressed + // 1: Primary button (left) + // 2: Secondary button (right) + int32 buttons = 3; + + // The display device where the mouse event occurred. + // Omitting or using the value 0 indicates the main display. + int32 display = 4; +} + +message WheelEvent { + // The value indicating how much the mouse wheel is rotated. Scaled so that + // 120 equals to 1 wheel click. (120 is chosen as a multiplier often used to + // represent wheel movements less than 1 wheel click. e.g. + // https://doc.qt.io/qt-5/qwheelevent.html#angleDelta) Positive delta value + // is assigned to dx when the top of wheel is moved to left. Similarly + // positive delta value is assigned to dy when the top of wheel is moved + // away from the user. + int32 dx = 1; + int32 dy = 2; + + // The display device where the mouse event occurred. + // Omitting or using the value 0 indicates the main display. + int32 display = 3; +} + +// KeyboardEvent objects describe a user interaction with the keyboard; each +// event describes a single interaction between the user and a key (or +// combination of a key with modifier keys) on the keyboard. +// This follows the pattern as set by +// (javascript)[https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent] +// +// Note: that only keyCode, key, or text can be set and that the semantics +// will slightly vary. +message KeyboardEvent { + // Code types that the emulator can receive. Note that the emulator + // will do its best to translate the code to an evdev value that + // will be send to the emulator. This translation is based on + // the chromium translation tables. See + // (this)[https://android.googlesource.com/platform/external/qemu/+/refs/heads/emu-master-dev/android/android-grpc/android/emulation/control/keyboard/keycode_converter_data.inc] + // for details on the translation. + enum KeyCodeType { + Usb = 0; + Evdev = 1; + XKB = 2; + Win = 3; + Mac = 4; + } + + enum KeyEventType { + // Indicates that this keyevent should be send to the emulator + // as a key down event. Meaning that the key event will be + // translated to an EvDev event type and bit 11 (0x400) will be + // set before it is sent to the emulator. + keydown = 0; + + // Indicates that the keyevent should be send to the emulator + // as a key up event. Meaning that the key event will be + // translated to an EvDev event type and + // sent to the emulator. + keyup = 1; + + // Indicates that the keyevent will be send to the emulator + // as e key down event and immediately followed by a keyup event. + keypress = 2; + } + + // Type of keycode contained in the keyCode field. + KeyCodeType codeType = 1; + + // The type of keyboard event that should be sent to the emulator + KeyEventType eventType = 2; + + // This property represents a physical key on the keyboard (as opposed + // to the character generated by pressing the key). In other words, this + // property is a value which isn't altered by keyboard layout or the + // state of the modifier keys. This value will be interpreted by the + // emulator depending on the KeyCodeType. The incoming key code will be + // translated to an evdev code type and send to the emulator. + // The values in key and text will be ignored. + int32 keyCode = 3; + + // The value of the key pressed by the user, taking into consideration + // the state of modifier keys such as Shift as well as the keyboard + // locale and layout. This follows the w3c standard used in browsers. + // You can find an accurate description of valid values + // [here](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) + // + // Note that some keys can result in multiple evdev events that are + // delivered to the emulator. for example the Key "A" will result in a + // sequence: + // ["Shift", "a"] -> [0x2a, 0x1e] whereas "a" results in ["a"] -> [0x1e]. + // + // Not all documented keys are understood by android, and only printable + // ASCII [32-127) characters are properly translated. + // + // Keep in mind that there are a set of key values that result in android + // specific behavior + // [see](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values#Phone_keys): + // + // - "AppSwitch": Behaves as the "Overview" button in android. + // - "GoBack": The Back button. + // - "GoHome": The Home button, which takes the user to the phone's main + // screen (usually an application launcher). + // - "Power": The Power button. + string key = 4; + + // Series of utf8 encoded characters to send to the emulator. An attempt + // will be made to translate every character will an EvDev event type and + // send to the emulator as a keypress event. The values in keyCode, + // eventType, codeType and key will be ignored. + // + // Note that most printable ASCII characters (range [32-127) can be send + // individually with the "key" param. Do not expect arbitrary UTF symbols to + // arrive in the emulator (most will be ignored). + // + // Note that it is possible to overrun the keyboard buffer by slamming this + // endpoint with large quantities of text (>1kb). The clipboard api is + // better suited for transferring large quantities of text. + string text = 5; +} + +// An input event that can be delivered to the emulator. +message InputEvent { + oneof type { + KeyboardEvent key_event = 1; + TouchEvent touch_event = 2; + MouseEvent mouse_event = 3; + AndroidEvent android_event = 4; + PenEvent pen_event = 5; + WheelEvent wheel_event = 6; + } +}; + +// The android input event system is a framework for handling input from a +// variety of devices by generating events that describe changes in the +// state of the devices and forwarding them to user space applications. +// +// An AndroidEvents will be delivered directly to the kernel as is. +message AndroidEvent { + // The type of the event. The types of the event are specified + // by the android kernel. Some examples are: + // EV_SYN, EV_KEY, EV_SW, etc.. + // The exact definitions can be found in the input.h header file. + int32 type = 1; + + // The actual code to be send to the kernel. The actual meaning + // of the code depends on the type definition. + int32 code = 2; + + // The actual value of the event. + int32 value = 3; + + // The display id associated with this input event. + int32 display = 4; +}; + +message Fingerprint { + // True when the fingprint is touched. + bool isTouching = 1; + + // The identifier of the registered fingerprint. + int32 touchId = 2; +} + +message GpsState { + // Setting this to false will disable auto updating from the LocationUI, + // otherwise the location UI will override the location at a frequency of + // 1hz. + // + // - This is unused if the emulator is launched with -no-window, or when he + // location ui is disabled. + // - This will BREAK the location ui experience if it is set to false. For + // example routing will no longer function. + bool passiveUpdate = 1; + + // The latitude, in degrees. + double latitude = 2; + + // The longitude, in degrees. + double longitude = 3; + + // The speed if it is available, in meters/second over ground + double speed = 4; + + // gets the horizontal direction of travel of this device, and is not + // related to the device orientation. It is guaranteed to be in the + // range [0.0, 360.0] if the device has a bearing. 0=North, 90=East, + // 180=South, etc.. + double bearing = 5; + + // The altitude if available, in meters above the WGS 84 reference + // ellipsoid. + double altitude = 6; + + // The number of satellites used to derive the fix + int32 satellites = 7; +} + +message BatteryState { + enum BatteryStatus { + UNKNOWN = 0; + CHARGING = 1; + DISCHARGING = 2; + NOT_CHARGING = 3; + FULL = 4; + } + + enum BatteryCharger { + NONE = 0; + AC = 1; + USB = 2; + WIRELESS = 3; + } + + enum BatteryHealth { + GOOD = 0; + FAILED = 1; + DEAD = 2; + OVERVOLTAGE = 3; + OVERHEATED = 4; + } + + bool hasBattery = 1; + bool isPresent = 2; + BatteryCharger charger = 3; + int32 chargeLevel = 4; + BatteryHealth health = 5; + BatteryStatus status = 6; +} + +// An ImageTransport allows for specifying a side channel for +// delivering image frames versus using the standard bytes array that is +// returned with the gRPC request. +message ImageTransport { + enum TransportChannel { + // Return full frames over the gRPC transport + TRANSPORT_CHANNEL_UNSPECIFIED = 0; + + // Write images to the a file/shared memory handle. + MMAP = 1; + } + + // The desired transport channel used for delivering image frames. Only + // relevant when streaming screenshots. + TransportChannel channel = 1; + + // Handle used for writing image frames if transport is mmap. The client + // sets and owns this handle. It can be either a shm region, or a mmap. A + // mmap should be a url that starts with `file:///` Note: the mmap can + // result in tearing. + string handle = 2; +} + +// The aspect ratio (width/height) will be different from the one +// where the device is unfolded. +message FoldedDisplay { + uint32 width = 1; + uint32 height = 2; + // It is possible for the screen to be folded in different ways depending + // on which surface is shown to the user. So xOffset and yOffset indicate + // the top left corner of the folded screen within the original unfolded + // screen. + uint32 xOffset = 3; + uint32 yOffset = 4; +} + +message ImageFormat { + enum ImgFormat { + // Portable Network Graphics format + // (https://en.wikipedia.org/wiki/Portable_Network_Graphics) + PNG = 0; + + // Three-channel RGB color model supplemented with a fourth alpha + // channel. https://en.wikipedia.org/wiki/RGBA_color_model + // Each pixel consists of 4 bytes. + RGBA8888 = 1; + + // Three-channel RGB color model, each pixel consists of 3 bytes + RGB888 = 2; + } + + // The (desired) format of the resulting bytes. + ImgFormat format = 1; + + // [Output Only] The rotation of the image. The image will be rotated + // based upon the coarse grained orientation of the device. + Rotation rotation = 2; + + // The (desired) width of the image. When passed as input + // the image will be scaled to match the given + // width, while maintaining the aspect ratio of the device. + // The returned image will never exceed the given width, but can be less. + // Omitting this value (or passing in 0) will result in no scaling, + // and the width of the actual device will be used. + uint32 width = 3; + + // The (desired) height of the image. When passed as input + // the image will be scaled to match the given + // height, while maintaining the aspect ratio of the device. + // The returned image will never exceed the given height, but can be less. + // Omitting this value (or passing in 0) will result in no scaling, + // and the height of the actual device will be used. + uint32 height = 4; + + // The (desired) display id of the device. Setting this to 0 (or omitting) + // indicates the main display. + uint32 display = 5; + + // Set this if you wish to use a different transport channel to deliver + // image frames. + ImageTransport transport = 6; + + // [Output Only] Display configuration when screen is folded. The value is + // the original configuration before scaling. + FoldedDisplay foldedDisplay = 7; + + // [Output Only] Display mode when AVD is resizable. + DisplayModeValue displayMode = 8; +} + +message Image { + ImageFormat format = 1; + + uint32 width = 2 [deprecated = true]; // width is contained in format. + uint32 height = 3 [deprecated = true]; // height is contained in format. + + // The organization of the pixels in the image buffer is from left to + // right and bottom up. This will be empty if an alternative image transport + // is requested in the image format. In that case the side channel should + // be used to obtain the image data. + bytes image = 4; + + // [Output Only] Monotonically increasing sequence number in a stream of + // screenshots. The first screenshot will have a sequence of 0. A single + // screenshot will always have a sequence number of 0. The sequence is not + // necessarily contiguous, and can be used to detect how many frames were + // dropped. An example sequence could be: [0, 3, 5, 7, 9, 11]. + uint32 seq = 5; + + // [Output Only] Unix timestamp in microseconds when the emulator estimates + // the frame was generated. The timestamp is before the actual frame is + // copied and transformed. This can be used to calculate variance between + // frame production time, and frame depiction time. + uint64 timestampUs = 6; +} + +message Rotation { + enum SkinRotation { + PORTRAIT = 0; // 0 degrees + LANDSCAPE = 1; // 90 degrees + REVERSE_PORTRAIT = 2; // -180 degrees + REVERSE_LANDSCAPE = 3; // -90 degrees + } + + // The rotation of the device, derived from the sensor state + // of the emulator. The derivation reflects how android observes + // the rotation state. + SkinRotation rotation = 1; + + // Specifies the angle of rotation, in degrees [-180, 180] + double xAxis = 2; + double yAxis = 3; + double zAxis = 4; +} + +message PhoneCall { + enum Operation { + InitCall = 0; + AcceptCall = 1; + RejectCallExplicit = 2; + RejectCallBusy = 3; + DisconnectCall = 4; + PlaceCallOnHold = 5; + TakeCallOffHold = 6; + } + Operation operation = 1; + string number = 2; +} + +message PhoneResponse { + enum Response { + OK = 0; + BadOperation = 1; // Enum out of range + BadNumber = 2; // Mal-formed telephone number + InvalidAction = 3; // E.g., disconnect when no call is in progress + ActionFailed = 4; // Internal error + RadioOff = 5; // Radio power off + } + Response response = 1; +} + +message Entry { + string key = 1; + string value = 2; +} + +message EntryList { + repeated Entry entry = 1; +} + +message EmulatorStatus { + // The emulator version string. + string version = 1; + + // The time the emulator has been active in .ms + uint64 uptime = 2; + + // True if the device has completed booting. + // For P and later this information will accurate, + // for older images we rely on adb. + bool booted = 3; + + // The current vm configuration + VmConfiguration vmConfig = 4; + + // The hardware configuration of the running emulator as + // key valure pairs. + EntryList hardwareConfig = 5; +} + +message AudioFormat { + enum SampleFormat { + AUD_FMT_U8 = 0; // Unsigned 8 bit + AUD_FMT_S16 = 1; // Signed 16 bit (little endian) + } + + enum Channels { + Mono = 0; + Stereo = 1; + } + + enum DeliveryMode { + // The audio queue will block and wait until the emulator requests + // packets. The client does not have to throttle and can push packets at + // will. This can result in the client falling behind. + MODE_UNSPECIFIED = 0; + // Audio packets will be delivered in real time (when possible). The + // audio queue will be overwritten with incoming data if data is made + // available. This means the client needs to control timing properly, or + // packets will get overwritten. + MODE_REAL_TIME = 1; // + } + // Sampling rate to use, defaulting to 44100 if this is not set. + // Note, that android devices typically will not use a sampling + // rate higher than 48kHz. See + // https://developer.android.com/ndk/guides/audio. + uint64 samplingRate = 1; + Channels channels = 2; + SampleFormat format = 3; + + // [Input Only] + // The mode used when delivering audio packets. + DeliveryMode mode = 4; +} + +message AudioPacket { + AudioFormat format = 1; + + // Unix epoch in us when this frame was captured. + uint64 timestamp = 2; + + // Contains a sample in the given audio format. + bytes audio = 3; +} + +message SmsMessage { + // The source address where this message came from. + // + // The address should be a valid GSM-formatted address as specified by + // 3GPP 23.040 Sec 9.1.2.5. + // + // For example: +3106225412 or (650) 555-1221 + string srcAddress = 1; + + // A utf8 encoded text message that should be delivered. + string text = 2; +} + +// A DisplayConfiguration describes a primary or secondary +// display available to the emulator. The screen aspect ratio +// cannot be longer (or wider) than 21:9 (or 9:21). Screen sizes +// larger than 4k will be rejected. +// +// Common configurations (w x h) are: +// - 480p (480x720) 142 dpi +// - 720p (720x1280) 213 dpi +// - 1080p (1080x1920) 320 dpi +// - 4K (2160x3840) 320 dpi +// - 4K (2160x3840) 640 dpi (upscaled) +// +// The behavior of the virtual display depends on the flags that are provided to +// this method. By default, virtual displays are created to be private, +// non-presentation and unsecure. +message DisplayConfiguration { + // These are the set of known android flags and their respective values. + // you can combine the int values to (de)construct the flags field below. + enum DisplayFlags { + DISPLAYFLAGS_UNSPECIFIED = 0; + + // When this flag is set, the virtual display is public. + // A public virtual display behaves just like most any other display + // that is connected to the system such as an external or wireless + // display. Applications can open windows on the display and the system + // may mirror the contents of other displays onto it. see: + // https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_PUBLIC + VIRTUAL_DISPLAY_FLAG_PUBLIC = 1; + + // When this flag is set, the virtual display is registered as a + // presentation display in the presentation display category. + // Applications may automatically project their content to presentation + // displays to provide richer second screen experiences. + // https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_PRESENTATION + VIRTUAL_DISPLAY_FLAG_PRESENTATION = 2; + + // When this flag is set, the virtual display is considered secure as + // defined by the Display#FLAG_SECURE display flag. The caller promises + // to take reasonable measures, such as over-the-air encryption, to + // prevent the contents of the display from being intercepted or + // recorded on a persistent medium. + // see: + // https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_SECURE + VIRTUAL_DISPLAY_FLAG_SECURE = 4; + + // This flag is used in conjunction with VIRTUAL_DISPLAY_FLAG_PUBLIC. + // Ordinarily public virtual displays will automatically mirror the + // content of the default display if they have no windows of their own. + // When this flag is specified, the virtual display will only ever show + // its own content and will be blanked instead if it has no windows. See + // https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY + VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = 8; + + // Allows content to be mirrored on private displays when no content is + // being shown. + // This flag is mutually exclusive with + // VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY. If both flags are specified + // then the own-content only behavior will be applied. + // see: + // https://developer.android.com/reference/android/hardware/display/DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) + VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR = 16; + } + + // The width of the display, restricted to: + // 320 * (dpi / 160) <= width + uint32 width = 1; + + // The heigh of the display, restricted to: + // * 320 * (dpi / 160) <= height + uint32 height = 2; + + // The pixel density (dpi). + // See https://developer.android.com/training/multiscreen/screendensities + // for details. This value should be in the range [120, ..., 640] + uint32 dpi = 3; + + // A combination of virtual display flags. These flags can be constructed + // by combining the DisplayFlags enum described above. + // + // The behavior of the virtual display depends on the flags. By default + // virtual displays are created to be private, non-presentation and + // unsecure. + uint32 flags = 4; + + // The id of the display. + // The primary (default) display has the display ID of 0. + // A secondary display has a display ID not 0. + // + // A display with the id in the range [1, userConfigurable] + // can be modified. See DisplayConfigurations below for details. + // + // The id can be used to get or stream a screenshot. + uint32 display = 5; +} +// Provides information about all the displays that can be attached +// to the emulator. The emulator will always have at least one display. +// +// The emulator usually has the following display configurations: +// 0: The default display. +// 1 - 3: User configurable displays. These can be added/removed. +// For example the standalone emulator allows you to modify these +// in the extended controls. +// 6 - 11: Fixed external displays. For example Android Auto uses fixed +// displays in this range. +message DisplayConfigurations { + repeated DisplayConfiguration displays = 1; + + // Display configurations with id [1, userConfigurable] are + // user configurable, that is they can be added, removed or + // updated. + uint32 userConfigurable = 2; + + // The maximum number of attached displays this emulator supports. + // This is the total number of displays that can be attached to + // the emulator. + // + // Note: A display with an id that is larger than userConfigurable cannot + // be modified. + uint32 maxDisplays = 3; +} + +message Notification { + enum EventType { + VIRTUAL_SCENE_CAMERA_INACTIVE = 0; + VIRTUAL_SCENE_CAMERA_ACTIVE = 1; + + // Fired when an update to a display event has been fired through + // the extended ui. This does not fire events when the display + // is changed through the console or gRPC endpoint. + DISPLAY_CONFIGURATIONS_CHANGED_UI = 2; + + // Don't add new event types here, add them to "oneof type" below. + } + + // Deprecated, use the type below to get detailed information + // regarding the event. + EventType event = 1 [deprecated = true]; + + // Detailed notification information. + oneof type { + CameraNotification cameraNotification = 2; + DisplayConfigurationsChangedNotification + displayConfigurationsChangedNotification = 3; + Posture posture = 4; + BootCompletedNotication booted = 5; + BrightnessValue brightness = 6; + } +} + +message BootCompletedNotication { + // The time in milliseconds it took for the boot to complete. + // Note that this value can be 0 when you are loading from a snapshot. + int32 time = 1; +} + +// Fired when the virtual scene camera is activated or deactivated and also in +// response to the streamNotification call. +message CameraNotification { + // Indicates whether the camera app was activated or deactivated. + bool active = 1; + // The display the camera app is associated with. + int32 display = 2; +} + +// Fired when an update to a display event has been fired through the extended +// ui. This does not fire events when the display is changed through the console +// or the gRPC endpoint. +message DisplayConfigurationsChangedNotification { + DisplayConfigurations displayConfigurations = 1; +} + +message RotationRadian { + float x = 1; // x axis is horizontal and orthogonal to the view direction. + float y = 2; // y axis points up and is perpendicular to the floor. + float z = 3; // z axis is the view direction and is set to 0.0 in + // rotateVirtualSceneCamera call. +} + +message Velocity { + float x = 1; // x axis is horizontal and orthogonal to the view direction. + float y = 2; // y axis points up and is perpendicular to the floor. + float z = 3; // z axis is the view direction +} + +// Must follow the definition in "external/qemu/android/hw-sensors.h" +message Posture { + enum PostureValue { + POSTURE_UNKNOWN = 0; + POSTURE_CLOSED = 1; + POSTURE_HALF_OPENED = 2; + POSTURE_OPENED = 3; + POSTURE_FLIPPED = 4; + POSTURE_TENT = 5; + POSTURE_MAX = 6; + } + PostureValue value = 3; +} + +message PhoneNumber { + // + // The phone number should be a valid GSM-formatted number as specified by + // 3GPP 23.040 Sec 9.1.2.5. + // + // For example: +3106225412 or (650) 555-1221 + string number = 1; +} \ No newline at end of file diff --git a/emulator-proto/src/main/proto/roborazzi.proto b/emulator-proto/src/main/proto/roborazzi.proto new file mode 100644 index 00000000..52f650c3 --- /dev/null +++ b/emulator-proto/src/main/proto/roborazzi.proto @@ -0,0 +1,38 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// 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. + +// Note that if you add/remove methods in this file you must update +// the metrics sql as well by running ./android/scripts/gen-grpc-sql.py +// +// Please group deleted methods in a block including the date (MM/DD/YY) +// it was removed. This enables us to easily keep metrics around after removal +// +// List of deleted methods +// rpc iWasDeleted (03/12/12) +// ... +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "roborazzi.emulator"; + +package roborazzi.emulator; +import "google/protobuf/empty.proto"; + +service Roborazzi { + rpc launchComposePreview(ComposePreview) returns (google.protobuf.Empty) {} +} + +message ComposePreview { + string previewMethod = 1; +} diff --git a/emulator-proto/src/main/proto/rtc_service.proto b/emulator-proto/src/main/proto/rtc_service.proto new file mode 100644 index 00000000..c5491cf3 --- /dev/null +++ b/emulator-proto/src/main/proto/rtc_service.proto @@ -0,0 +1,117 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// 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. + +// Note that if you add/remove methods in this file you must update +// the metrics sql as well by running ./android/scripts/gen-grpc-sql.py +// +// Please group deleted methods in a block including the date (MM/DD/YY) +// it was removed. This enables us to easily keep metrics around after removal +// +// List of deleted methods +// rpc iWasDeleted (03/12/12) +// ... +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "com.android.emulator.control"; +option objc_class_prefix = "AEC"; + +package android.emulation.control; +import "google/protobuf/empty.proto"; + +// An RTC service lets you interact with the emulator through WebRTC +// Note that this is currently an experimental feature, and that the +// service definition might change without notice. Use at your own risk! +// +// The following endpoints are needed to establish the webrtc protocol +// Due to limitiations in Javascript we cannot make use of bidirectional +// endpoints See this [blog](https://grpc.io/blog/state-of-grpc-web) for +// details. +service Rtc { + // This function will generate a new identifier that the client + // should use for further interaction. It will initiate the + // JSEP protocol on the server side. + rpc requestRtcStream(google.protobuf.Empty) returns (RtcId) {} + + // Sends the given JsepMsg to the server. The RtcId in the + // message should point to an active stream negotiation in + // progress, otherwise the message will be ignored. + rpc sendJsepMessage(JsepMsg) returns (google.protobuf.Empty) {} + + // Reads an available jsep messages for the given client id, + // blocking until one becomes available. Do not use the polling version + // above if you opt for this one. + // + // The ice candidates for example will trickle in on this callback, + // as will the SDP negotation. + rpc receiveJsepMessages(RtcId) returns (stream JsepMsg) {} + + + // [DEPRECATED] This is only here as the go grpc webproxy used + // by fuchsia does not support server side streaming. This method + // will be removed in the future and should not be relied upon. + // + // Reads an available jsep messages for the given client id, + // blocking until one becomes available. Do not use the polling version + // above if you opt for this one. + // + // The ice candidates for example will trickle in on this callback, + // as will the SDP negotation. + rpc receiveJsepMessage(RtcId) returns (JsepMsg) {} +} + +message RtcId { + // The unique identifier of this connection. You will have to use the + // same identifier when sending/receiving messages. The server will + // generate a guid when receiving the start message. + string guid = 1; +} + +message JsepMsg { + // The unique identifier of this connection. You will have to use the + // same identifier when sending/receiving messages. The server will + // generate a guid when receiving the start message. + RtcId id = 1; + // The JSON payload. This usually can be directly handled by the + // Javascript library. + // + // The dictionary can contain the following properties + // + // - bye: + // You can hang up now. No new message expected for you. + // The server has stopped the RTC stream. + // + // - start: + // An RTCConfiguration dictionary providing options to + // configure the new connection. This can include the + // turn configuration the serve is using. This dictionary can be + // passed in directly to the + // [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) + // object. + // + // - candidate: + // The WebRTC API's RTCIceCandidateInit dictionary, which + // contains the information needed to fundamentally describe an + // RTCIceCandidate. See + // [RTCIceCandidate](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate) + // and [Session + // Lifetime](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Session_lifetime) + // for more details. + // + // - sdp: + // RTCSessionDescriptionInit dictionary containing the values + // to that can be assigned to a + // [RTCSessionDescription](https://developer.mozilla.org/en-US/docs/Web/API/RTCSessionDescription) + string message = 2; +} diff --git a/emulator-server/.gitignore b/emulator-server/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/emulator-server/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/emulator-server/build.gradle.kts b/emulator-server/build.gradle.kts new file mode 100644 index 00000000..ae61992b --- /dev/null +++ b/emulator-server/build.gradle.kts @@ -0,0 +1,99 @@ +@file:Suppress("UnstableApiUsage") + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("com.android.library") + kotlin("android") + id("org.jetbrains.compose") + id("com.squareup.wire") version "4.9.9" +} + +android { + compileSdk = 34 + + defaultConfig { + minSdk = 21 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + buildFeatures { + buildConfig = false + } + + kotlinOptions { + jvmTarget = "11" + } + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } + + namespace = "com.github.takahirom.roborazzi.emulator.server" +} + +buildscript { + dependencies { + classpath(libs.server.generator) + } +} + +group = "com.github.takahirom.roborazzi.emulator.server" +version = "1.0-SNAPSHOT" + +dependencies { + api(libs.grpc.protobuf) + implementation(libs.server) + implementation(libs.androidx.monitor) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.core) + implementation(libs.grpc.kotlin.stub) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.material) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.ui.tooling) + implementation(libs.androidx.activity) + + // for use with shadows etc + implementation(libs.robolectric) + + protoPath(project(":emulator-proto")) + implementation(project(":emulator-proto")) + + testImplementation(libs.grpc.netty.shaded) + testImplementation(libs.junit) + testImplementation(libs.kotlinx.coroutines.test) +} + +wire { + sourcePath { + srcProject(":emulator-proto") + } + + custom { + schemaHandlerFactory = com.squareup.wire.kotlin.grpcserver.GrpcServerSchemaHandler.Factory() + options = mapOf( + "singleMethodServices" to "false", + "rpcCallStyle" to "suspending", + ) + exclusive = false + } + + kotlin { + rpcRole = "server" + singleMethodServices = false + rpcCallStyle = "suspending" + } +} + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs += "-Xcontext-receivers" + } +} \ No newline at end of file diff --git a/emulator-server/src/main/AndroidManifest.xml b/emulator-server/src/main/AndroidManifest.xml new file mode 100644 index 00000000..453a37b0 --- /dev/null +++ b/emulator-server/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/EmulatorControllerService.kt b/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/EmulatorControllerService.kt new file mode 100644 index 00000000..3a496fb5 --- /dev/null +++ b/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/EmulatorControllerService.kt @@ -0,0 +1,152 @@ +package com.github.takahirom.roborazzi.emulator + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.graphics.Bitmap +import android.graphics.Bitmap.Config +import android.location.Location +import android.location.LocationManager +import android.os.BatteryManager +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.view.PixelCopy +import android.view.View +import android.view.WindowManager +import androidx.core.content.ContextCompat +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry +import androidx.test.runner.lifecycle.Stage +import com.android.emulator.control.BatteryState +import com.android.emulator.control.EmulatorControllerWireGrpc +import com.android.emulator.control.GpsState +import com.android.emulator.control.Image +import com.android.emulator.control.ImageFormat +import com.android.emulator.control.LogMessage +import com.github.takahirom.roborazzi.emulator.RoboInstances.executeOnMain +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.yield +import okio.ByteString.Companion.toByteString +import org.robolectric.Shadows +import org.robolectric.shadows.ShadowWindowManagerImpl +import java.io.ByteArrayOutputStream + + +class EmulatorControllerService(val dispatcher: CoroutineDispatcher) : + EmulatorControllerWireGrpc.EmulatorControllerImplBase() { + + val androidContext: Context + get() = InstrumentationRegistry.getInstrumentation().targetContext + + inline fun systemService(): T { + return ContextCompat.getSystemService(androidContext, T::class.java)!! + } + + override suspend fun getBattery(request: Unit): BatteryState = executeOnMain { + val batteryManager = systemService() + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return@executeOnMain BatteryState() + } + + val statusProperty = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS) + val capacityProperty = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) + + val status = when (statusProperty) { + BatteryManager.BATTERY_STATUS_FULL -> BatteryState.BatteryStatus.FULL + BatteryManager.BATTERY_STATUS_CHARGING -> BatteryState.BatteryStatus.CHARGING + BatteryManager.BATTERY_STATUS_DISCHARGING -> BatteryState.BatteryStatus.DISCHARGING + BatteryManager.BATTERY_STATUS_NOT_CHARGING -> BatteryState.BatteryStatus.NOT_CHARGING + else -> BatteryState.BatteryStatus.UNKNOWN + } + BatteryState( + hasBattery = true, + isPresent = true, + charger = BatteryState.BatteryCharger.USB, + chargeLevel = capacityProperty, + status = status + ) + } + + @SuppressLint("MissingPermission") + override suspend fun getGps(request: Unit): GpsState = executeOnMain { + val locationManager = systemService() + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + return@executeOnMain GpsState() + } + + println(GrpcMetadata.instanceContextKey.get()) + + val providers = locationManager.getProviders(true) + println(providers) + + val location = locationManager.getLastKnownLocation("gps") + + GpsState( + latitude = location?.latitude ?: 0.0, + longitude = location?.longitude ?: 0.0 + ) + } + + override suspend fun setGps(request: GpsState) = executeOnMain { + val locationManager = systemService() + + val shadow = Shadows.shadowOf(locationManager) + + shadow.simulateLocation(Location("gps").apply { + latitude = request.latitude + longitude = request.longitude + }) + } + + @SuppressLint("NewApi") + override suspend fun getScreenshot(request: ImageFormat): Image = executeOnMain { + val monitor = ActivityLifecycleMonitorRegistry.getInstance() + val activities = monitor.getActivitiesInStage(Stage.RESUMED) + println(activities) + val activity = activities.first() + + val window = activity.window + val view = window.findViewById(android.R.id.content) + val bitmap = Bitmap.createBitmap(view.width, view.height, Config.ARGB_8888) + val result = CompletableDeferred() + + println("copying") + PixelCopy.request( + window!!, + bitmap, + { + println("copied $it") + val output = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, output) + println("compressed") + val image = Image( + format = ImageFormat(format = ImageFormat.ImgFormat.PNG), + width = view.width, + height = view.height, + image = output.toByteArray().toByteString() + ) + result.complete(image) + }, + Handler(Looper.getMainLooper()) + ) + + println("awaiting") + result.await().also { + println("returning Image") + } + } + + override fun streamLogcat(request: LogMessage): Flow = channelFlow { + repeat(100) { + trySend(LogMessage("Hello $it")) + yield() + } + } +} diff --git a/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/RoboInstances.kt b/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/RoboInstances.kt new file mode 100644 index 00000000..92da6a01 --- /dev/null +++ b/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/RoboInstances.kt @@ -0,0 +1,25 @@ +package com.github.takahirom.roborazzi.emulator + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +object RoboInstances { + suspend fun executeOnMain(fn: suspend () -> T): T { + println("${Thread.currentThread()} before executeOnMain") + return withContext(Dispatchers.Main) { + println("${Thread.currentThread()} executeOnMain") + try { + fn().also { + println("${Thread.currentThread()} executeOnMain done") + } + } catch (e: Exception) { + e.printStackTrace() + throw e + } + } + } + + suspend fun runWork() { + // TODO contribute this thread per instance + } +} \ No newline at end of file diff --git a/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/RoborazziService.kt b/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/RoborazziService.kt new file mode 100644 index 00000000..fa5002e7 --- /dev/null +++ b/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/RoborazziService.kt @@ -0,0 +1,42 @@ +package com.github.takahirom.roborazzi.emulator + +import android.content.Context +import android.content.Intent +import androidx.compose.ui.tooling.PreviewActivity +import androidx.core.content.ContextCompat +import androidx.test.platform.app.InstrumentationRegistry +import com.github.takahirom.roborazzi.emulator.RoboInstances.executeOnMain +import kotlinx.coroutines.CoroutineDispatcher +import org.robolectric.Robolectric +import roborazzi.emulator.ComposePreview +import roborazzi.emulator.RoborazziWireGrpc + + +class RoborazziService(val dispatcher: CoroutineDispatcher) : + RoborazziWireGrpc.RoborazziImplBase() { + + val androidContext: Context + get() = InstrumentationRegistry.getInstrumentation().targetContext + + inline fun systemService(): T { + return ContextCompat.getSystemService(androidContext, T::class.java)!! + } + + override suspend fun launchComposePreview(request: ComposePreview) = executeOnMain { + println("$request") + + try { + val activityController = Robolectric.buildActivity(PreviewActivity::class.java) + + activityController.newIntent(Intent().apply { + putExtra("composable", request.previewMethod) + }) + activityController.setup() + + val activity: PreviewActivity = activityController.get() + println(activity.intent) + } catch (e: Exception) { + e.printStackTrace() + } + } +} diff --git a/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/RtcService.kt b/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/RtcService.kt new file mode 100644 index 00000000..091ee910 --- /dev/null +++ b/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/RtcService.kt @@ -0,0 +1,6 @@ +package com.github.takahirom.roborazzi.emulator + +import com.android.emulator.control.RtcWireGrpc + +class RtcService: RtcWireGrpc.RtcImplBase() { +} \ No newline at end of file diff --git a/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/previews/SimplePreview.kt b/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/previews/SimplePreview.kt new file mode 100644 index 00000000..f1544087 --- /dev/null +++ b/emulator-server/src/main/kotlin/com/github/takahirom/roborazzi/emulator/previews/SimplePreview.kt @@ -0,0 +1,29 @@ +package com.github.takahirom.roborazzi.emulator.previews; + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview + +@Composable +fun Simple() { + println("Simple") + Box(Modifier.fillMaxSize()) { + Text("Simple") + + } + + LaunchedEffect(Unit) { + println("Launched Effect") + } +} + +@Composable +@Preview +fun SimplePreview() { + println("SimplePreview") + Simple() +} diff --git a/emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/GrpcServer.kt b/emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/GrpcServer.kt new file mode 100644 index 00000000..f4ba44c9 --- /dev/null +++ b/emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/GrpcServer.kt @@ -0,0 +1,57 @@ +package com.github.takahirom.roborazzi.emulator + +import com.github.takahirom.roborazzi.emulator.GrpcMetadata.instanceContextKey +import com.github.takahirom.roborazzi.emulator.GrpcMetadata.instanceMetadataKey +import io.grpc.Context +import io.grpc.Contexts +import io.grpc.Metadata +import io.grpc.ServerBuilder +import io.grpc.ServerCall +import io.grpc.ServerCallHandler +import io.grpc.ServerInterceptor +import io.grpc.ServerRegistry +import io.grpc.netty.shaded.io.grpc.netty.NettyServerProvider +import kotlinx.coroutines.Dispatchers +import okio.Closeable + +class GrpcServer : Closeable { + val emulatorControllerService = EmulatorControllerService(Dispatchers.Main) + val roborazziService = RoborazziService(Dispatchers.Main) + + val server by lazy { + ServerBuilder.forPort(8080) + .addService(emulatorControllerService) + .addService(roborazziService) + .addService(RtcService()) + .intercept(object : ServerInterceptor { + override fun interceptCall( + call: ServerCall, + headers: Metadata, + next: ServerCallHandler + ): ServerCall.Listener { + println("Call: ${call.methodDescriptor.bareMethodName}") + + val instance = headers[instanceMetadataKey] + + val newContext = Context.current().withValue(instanceContextKey, instance) + + return Contexts.interceptCall(newContext, call, headers, next) + } + }) + .build() + } + + override fun close() { + server.shutdownNow() + } + + fun start() { + server.start() + } + + companion object { + init { + ServerRegistry.getDefaultRegistry().register(NettyServerProvider()) + } + } +} \ No newline at end of file diff --git a/emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/Main.kt b/emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/Main.kt new file mode 100644 index 00000000..c98f20e3 --- /dev/null +++ b/emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/Main.kt @@ -0,0 +1,38 @@ +package com.github.takahirom.roborazzi.emulator + +import io.grpc.Metadata +import io.grpc.ServerBuilder +import io.grpc.ServerCall +import io.grpc.ServerCallHandler +import io.grpc.ServerInterceptor +import io.grpc.ServerRegistry +import io.grpc.netty.shaded.io.grpc.netty.NettyServerProvider +import kotlinx.coroutines.Dispatchers + +suspend fun main() { + val roboEnv = RoboEnv() + + ServerRegistry.getDefaultRegistry().register(NettyServerProvider()) + + val emulatorControllerService = EmulatorControllerService(Dispatchers.Main) + + println(emulatorControllerService.getBattery(Unit)) + + val server = ServerBuilder.forPort(8080) + .addService(emulatorControllerService) + .addService(RtcService()) + .intercept(object : ServerInterceptor { + override fun interceptCall( + call: ServerCall, + headers: Metadata, + next: ServerCallHandler + ): ServerCall.Listener { + println("Call: ${call.methodDescriptor.bareMethodName}") + return next.startCall(call, headers) + } + }) + .build() + + server.start() + server.awaitTermination() +} diff --git a/emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/RoboEmulatorServerTest.kt b/emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/RoboEmulatorServerTest.kt new file mode 100644 index 00000000..b48403b7 --- /dev/null +++ b/emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/RoboEmulatorServerTest.kt @@ -0,0 +1,42 @@ +package com.github.takahirom.roborazzi.emulator + +import android.os.Looper +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +@Config(sdk = [34], qualifiers = "w320dp-h533dp-normal-long-notround-any-hdpi-keyshidden-trackball") +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@RunWith(RobolectricTestRunner::class) +class RoboEmulatorServerTest { + @Test + fun testOperations() = runTest { + val server = GrpcServer() + + server.start() + + println(server.emulatorControllerService.getBattery(Unit)) + println(server.emulatorControllerService.getGps(Unit)) + + server.close() + } + + + @Test + fun testServer() { + val server = GrpcServer() + + println("Starting on 8080") + server.start() + println("Started") + + val looper = Shadows.shadowOf(Looper.getMainLooper()) + while (true) { + looper.idle() + } + } +} \ No newline at end of file diff --git a/emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/RoboEnv.kt b/emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/RoboEnv.kt new file mode 100644 index 00000000..80bbf8e7 --- /dev/null +++ b/emulator-server/src/test/kotlin/com/github/takahirom/roborazzi/emulator/RoboEnv.kt @@ -0,0 +1,95 @@ +package com.github.takahirom.roborazzi.emulator + +import org.robolectric.android.AndroidSdkShadowMatcher +import org.robolectric.annotation.GraphicsMode +import org.robolectric.annotation.LooperMode +import org.robolectric.annotation.SQLiteMode +import org.robolectric.interceptors.AndroidInterceptors +import org.robolectric.internal.ResourcesMode +import org.robolectric.internal.SandboxManager +import org.robolectric.internal.ShadowProvider +import org.robolectric.internal.bytecode.InstrumentationConfiguration +import org.robolectric.internal.bytecode.Interceptors +import org.robolectric.internal.bytecode.ShadowMap +import org.robolectric.internal.bytecode.ShadowWrangler +import org.robolectric.internal.dependency.MavenDependencyResolver +import org.robolectric.manifest.AndroidManifest +import org.robolectric.pluginapi.Sdk +import org.robolectric.pluginapi.SdkProvider +import org.robolectric.plugins.DefaultSdkProvider +import org.robolectric.plugins.HierarchicalConfigurationStrategy.ConfigurationImpl +import org.robolectric.plugins.SdkCollection +import org.robolectric.util.inject.Injector +import java.util.Properties +import java.util.ServiceLoader +import kotlin.io.path.Path + + +class RoboEnv { + val sdkProvider = DefaultSdkProvider(MavenDependencyResolver()) + val sdkCollection = SdkCollection(sdkProvider) + val sdk34 = sdkCollection.getSdk(34) + val injector = Injector.Builder() + .bind(Properties::class.java, System.getProperties()) + .bind(SdkProvider::class.java, sdkProvider) + .bind(SdkCollection::class.java, sdkCollection) + .bind(Injector.Key(Sdk::class.java, "runtimeSdk"), sdk34) + .bind(Injector.Key(Sdk::class.java, "compileSdk"), sdk34) + .bind(Injector.Key(Sdk::class.java, null), sdk34) + .build() + + val sandboxManager = injector.getInstance(SandboxManager::class.java) + + inline fun withSandbox(noinline function: () -> T): T { + val classLoaderConfig = + InstrumentationConfiguration.newBuilder() + .doNotAcquirePackage("java.") + .doNotAcquirePackage("javax.") + .doNotAcquirePackage("jdk.internal.") + .doNotAcquirePackage("sun.") + .doNotAcquirePackage("org.robolectric.annotation.") + .doNotAcquirePackage("org.robolectric.internal.") + .doNotAcquirePackage("org.robolectric.pluginapi.") + .doNotAcquirePackage("org.robolectric.util.") + .build() + + val sdk = sdk34 + + val resourcesMode = ResourcesMode.BINARY + val looperMode: LooperMode.Mode = LooperMode.Mode.INSTRUMENTATION_TEST + val sqliteMode = SQLiteMode.Mode.NATIVE + val graphicsMode = GraphicsMode.Mode.NATIVE + + val sandbox = sandboxManager.getAndroidSandbox( + classLoaderConfig, sdk, resourcesMode, looperMode, sqliteMode, graphicsMode + ) + + val shadowProviders = ServiceLoader.load( + ShadowProvider::class.java + ).toList() + val baseShadowMap = ShadowMap.createFromShadowProviders(shadowProviders) + + val interceptors = Interceptors(AndroidInterceptors.all()) + val classHandler = + ShadowWrangler(baseShadowMap, AndroidSdkShadowMatcher(sdk34.apiLevel), interceptors) + sandbox.configure(classHandler, interceptors) + + val configuration = ConfigurationImpl() + val appManifest = AndroidManifest( + Path("src/main/AndroidManifest.xml"), + Path("src/main/res"), + Path("src/main/assets") + ) + + sandbox.testEnvironment.setUpApplicationState( + RoboEnv::class.java.getMethod("aMethod"), + configuration, + appManifest + ) + + return sandbox.runOnMainThread(function) + } + + fun aMethod() { + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 3c47293d..528e475d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,62 +1,38 @@ -VERSION_NAME=1.18.0 -GROUP=io.github.takahirom.roborazzi -# Project-wide Gradle settings. - -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. - -# For more details on how to configure your build environment visit +## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html - +# # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 - -systemProp.org.gradle.internal.http.connectionTimeout=180000 -systemProp.org.gradle.internal.http.socketTimeout=180000 - -org.gradle.unsafe.configuration-cache=true - +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +# # When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects # org.gradle.parallel=true - -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true -# Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official - -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true - -SONATYPE_AUTOMATIC_RELEASE=true - -POM_NAME=Roborazzi +#Mon May 27 00:04:14 BST 2024 +GROUP=io.github.takahirom.roborazzi POM_DESCRIPTION=Make JVM Android integration test visible - -POM_URL=https://github.com/takahirom/roborazzi/ -POM_SCM_URL=https://github.com/takahirom/roborazzi/ -POM_SCM_CONNECTION=scm:git:git://github.com/takahirom/roborazzi.git -POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/takahirom/roborazzi.git - -POM_LICENCE_NAME=Apache-2.0 -POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0 -POM_LICENCE_DIST=repo - POM_DEVELOPER_ID=takahirom POM_DEVELOPER_NAME=takahirom -POM_DEVELOPER_URL=https://github.com/takahirom - -org.jetbrains.compose.experimental.uikit.enabled=true - +POM_DEVELOPER_URL=https\://github.com/takahirom +POM_LICENCE_DIST=repo +POM_LICENCE_NAME=Apache-2.0 +POM_LICENCE_URL=https\://www.apache.org/licenses/LICENSE-2.0 +POM_NAME=Roborazzi +POM_SCM_CONNECTION=scm\:git\:git\://github.com/takahirom/roborazzi.git +POM_SCM_DEV_CONNECTION=scm\:git\:ssh\://git@github.com/takahirom/roborazzi.git +POM_SCM_URL=https\://github.com/takahirom/roborazzi/ +POM_URL=https\://github.com/takahirom/roborazzi/ +SONATYPE_AUTOMATIC_RELEASE=true +VERSION_NAME=1.18.0 +android.enableJetifier=false +android.nonTransitiveRClass=true +android.useAndroidX=true +kotlin.code.style=official kotlin.incremental.native=true - -# To debug -#roborazzi.test.record=true -#roborazzi.test.verify=true +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding\=UTF-8 +org.gradle.unsafe.configuration-cache=true +org.jetbrains.compose.experimental.uikit.enabled=true +systemProp.org.gradle.internal.http.connectionTimeout=180000 +systemProp.org.gradle.internal.http.socketTimeout=180000 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0afb5b6a..655676fb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,16 @@ [versions] +core = "1.13.1" javaToolchain = "17" javaTarget = "11" agp = "7.3.1" commons-compress = "1.23.0" +grpcKotlinStub = "1.4.1" +grpcProtobuf = "1.62.2" kotlin = "1.9.22" mavenPublish = "0.25.3" composeCompiler = "1.5.10" composeMultiplatform = "1.6.2" +monitor = "1.7.0-beta01" robolectric = "4.12.2" robolectric-android-all = "Q-robolectric-5415296" @@ -38,10 +42,23 @@ google-android-material = "1.5.0" junit = "4.13.2" ktor-serialization-kotlinx-xml = "2.3.0" kotlinx-serialization = "1.6.3" +server = "1.0.0-alpha03" +serverGenerator = "1.0.0-alpha03" squareup-okhttp = "5.0.0-alpha.11" kotlinx-io = "0.3.3" +uiautomator = "2.3.0" + +kotlinxCoroutine = "1.8.0" +wireGrpcClient = "4.9.9" [libraries] +androidx-core = { module = "androidx.core:core", version.ref = "core" } +androidx-monitor = { module = "androidx.test:monitor", version.ref = "monitor" } +androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomator" } +grpc-kotlin-stub = { module = "io.grpc:grpc-kotlin-stub", version.ref = "grpcKotlinStub" } +grpc-netty-shaded = { module = "io.grpc:grpc-netty-shaded", version.ref = "grpcProtobuf" } +grpc-netty = { module = "io.grpc:grpc-netty", version.ref = "grpcProtobuf" } +grpc-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpcProtobuf" } roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi-for-replacing-by-include-build" } roborazzi-junit-rule = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi-for-replacing-by-include-build" } roborazzi-rule = { module = "io.github.takahirom.roborazzi:roborazzi-rule", version.ref = "roborazzi-for-replacing-by-include-build" } @@ -51,6 +68,12 @@ kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutine" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutine" } +kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutine" } +kotlinx-coroutines-playservices = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinxCoroutine" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutine" } + androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } @@ -85,8 +108,11 @@ google-android-material = { module = "com.google.android.material:material", ver junit = { module = "junit:junit", version.ref = "junit" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } ktor-serialization-kotlinx-xml = { module = "io.ktor:ktor-serialization-kotlinx-xml", version.ref = "ktor-serialization-kotlinx-xml" } +server = { module = "com.squareup.wiregrpcserver:server", version.ref = "server" } +server-generator = { module = "com.squareup.wiregrpcserver:server-generator", version.ref = "serverGenerator" } squareup-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "squareup-okhttp" } squareup-okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp-coroutines", version.ref = "squareup-okhttp" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } robolectric-android-all = { module = "org.robolectric:android-all", version.ref = "robolectric-android-all" } -kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" } \ No newline at end of file +kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" } +wire-grpc-client = { module = "com.squareup.wire:wire-grpc-client", version.ref = "wireGrpcClient" } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index c4108a23..82cc0970 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,6 +25,10 @@ include ':sample-android-without-compose' include ':sample-compose-desktop-multiplatform' include ':sample-compose-desktop-jvm' +include ':emulator-proto' +include ':emulator-server' +include ':emulator-client' + includeBuild("include-build") { dependencySubstitution { substitute(module("io.github.takahirom.roborazzi:roborazzi-gradle-plugin")).using(project(":roborazzi-gradle-plugin")) From 6daf4bdbe5f5d63ba46a16fb32336a5bf0f23975 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Mon, 27 May 2024 18:41:03 +0100 Subject: [PATCH 2/2] Preview --- emulator-proto/build.gradle.kts | 6 ----- emulator-proto/src/main/proto/roborazzi.proto | 23 ------------------- 2 files changed, 29 deletions(-) diff --git a/emulator-proto/build.gradle.kts b/emulator-proto/build.gradle.kts index de5794be..1ec65412 100644 --- a/emulator-proto/build.gradle.kts +++ b/emulator-proto/build.gradle.kts @@ -15,10 +15,4 @@ dependencies { wire { protoLibrary = true - -// kotlin { -// rpcCallStyle = "suspending" -// rpcRole = "client" -// singleMethodServices = false -// } } diff --git a/emulator-proto/src/main/proto/roborazzi.proto b/emulator-proto/src/main/proto/roborazzi.proto index 52f650c3..b3601b1d 100644 --- a/emulator-proto/src/main/proto/roborazzi.proto +++ b/emulator-proto/src/main/proto/roborazzi.proto @@ -1,26 +1,3 @@ -// Copyright (C) 2018 The Android Open Source Project -// -// 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. - -// Note that if you add/remove methods in this file you must update -// the metrics sql as well by running ./android/scripts/gen-grpc-sql.py -// -// Please group deleted methods in a block including the date (MM/DD/YY) -// it was removed. This enables us to easily keep metrics around after removal -// -// List of deleted methods -// rpc iWasDeleted (03/12/12) -// ... syntax = "proto3"; option java_multiple_files = true;