├── .gitattributes ├── MANIFEST.in ├── js ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── jwt-provider │ └── requirements.txt ├── develop │ ├── README.md │ ├── envoy.Dockerfile │ └── envoy.yaml ├── src │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── components │ │ ├── copyright.js │ │ ├── logcat_view.js │ │ ├── login_firebase.js │ │ └── emulator_screen.js │ ├── App.css │ ├── App.js │ ├── logo.svg │ ├── service │ │ └── auth_service.js │ └── serviceWorker.js ├── docker │ ├── production.yaml │ ├── development.yaml │ ├── nginx.Dockerfile │ ├── envoy.Dockerfile │ ├── emulator.service │ ├── docker-compose-build.yaml │ ├── docker-compose.yaml │ ├── certs │ │ ├── self_sign.crt │ │ └── self_sign.key │ └── envoy.yaml ├── firebase_config.json ├── .dockerignore ├── .gitignore ├── develop.sh ├── package.json ├── config_gen.py ├── Makefile ├── turn │ ├── turn.py │ └── README.MD └── README.md ├── emu ├── __init__.py ├── templates │ ├── README.md │ ├── default.pa │ ├── cloudbuild.README.MD │ ├── avd │ │ ├── Pixel2.ini │ │ └── Pixel2.avd │ │ │ └── config.ini │ ├── Dockerfile.system_image │ ├── Dockerfile.emulator │ ├── Dockerfile │ ├── emulator.README.MD │ ├── registry.README.MD │ └── launch-emulator.sh ├── utils.py ├── platform_tools.py ├── containers │ ├── progress_tracker.py │ ├── system_image_container.py │ ├── emulator_container.py │ └── docker_container.py ├── template_writer.py ├── docker_config.py ├── cloud_build.py └── android_release_zip.py ├── .pylintrc ├── aemu-container.code-workspace ├── setup.cfg ├── pyproject.toml ├── run.sh ├── tox.ini ├── tests ├── test_template_writer.py ├── test_platform_tools.py ├── conftest.py └── e2e │ ├── utils.py │ ├── test_docker_container.py │ ├── test_cloud_build.py │ └── test_containers.py ├── CONTRIBUTING.md ├── run-with-gpu.sh ├── configure.sh ├── run-in-script-example.sh ├── .gitignore ├── create_web_container.sh ├── REGISTRY.MD ├── cloud-init ├── cloud-init └── README.MD ├── TROUBLESHOOTING.md └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | emu/_version.py export-subst 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include emu/_version.py 3 | -------------------------------------------------------------------------------- /js/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/android-emulator-container-scripts/HEAD/js/public/favicon.ico -------------------------------------------------------------------------------- /emu/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import get_versions 2 | 3 | __version__ = get_versions()["version"] 4 | del get_versions 5 | -------------------------------------------------------------------------------- /js/jwt-provider/requirements.txt: -------------------------------------------------------------------------------- 1 | pyjwt 2 | flask 3 | flask-cors 4 | absl-py 5 | Flask-HTTPAuth==4.4.0 6 | Werkzeug 7 | JWCrypto 8 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" -------------------------------------------------------------------------------- /js/develop/README.md: -------------------------------------------------------------------------------- 1 | This contains an envoy configuration that can be used during development. 2 | It is configured as a gRPC proxy to the emulator running on port 8554. 3 | It will redirect the rest to the npm develop endpoint on port 3000 4 | 5 | 6 | -------------------------------------------------------------------------------- /emu/templates/README.md: -------------------------------------------------------------------------------- 1 | The (sub)directories here contain templates that will be used to create the final docker image. 2 | The layout should follow the desired layout in the container. 3 | 4 | Make sure to update setup.py if you add new directories here. 5 | -------------------------------------------------------------------------------- /js/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /aemu-container.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "js/src/android_emulation_control" 5 | }, 6 | { 7 | "path": "." 8 | } 9 | ], 10 | "settings": { 11 | "python.formatting.provider": "none", 12 | "[python]": { 13 | "editor.defaultFormatter": "ms-python.black-formatter" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | # This includes the license file(s) in the wheel. 3 | # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file 4 | license_files = LICENSE 5 | 6 | 7 | [versioneer] 8 | VCS = git 9 | style = pep440 10 | versionfile_source = emu/_version.py 11 | tag_prefix = 12 | parentdir_prefix = emu-docker- 13 | -------------------------------------------------------------------------------- /js/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /js/docker/production.yaml: -------------------------------------------------------------------------------- 1 | # This docker has a series of overrides to expose the private adbkey as a secret. 2 | # It requires that the /opt/emulator/adbkey file exists where you launch the container. 3 | version: "3.7" 4 | services: 5 | emulator: 6 | secrets: 7 | - adbkey 8 | ports: 9 | - "5555:5555" 10 | 11 | secrets: 12 | adbkey: 13 | file: /opt/emulator/adbkey 14 | 15 | -------------------------------------------------------------------------------- /js/docker/development.yaml: -------------------------------------------------------------------------------- 1 | # This docker has a series of overrides to expose the private adbkey as a secret. 2 | # It requires that the ~/.android/adbkey file exists where you launch the container. 3 | version: "3.7" 4 | services: 5 | emulator: 6 | secrets: 7 | - adbkey 8 | ports: 9 | - "5555:5555" 10 | - "5554:5554" 11 | 12 | 13 | secrets: 14 | adbkey: 15 | file: ~/.android/adbkey 16 | -------------------------------------------------------------------------------- /emu/templates/default.pa: -------------------------------------------------------------------------------- 1 | # This is a NOP configuration for pulse audio, all audio goes nowhere! 2 | load-module module-null-sink sink_name=NOP sink_properties=device.description=NOP 3 | 4 | # Make pulse accessible on all channels. We only have null audio, and Docker 5 | # should isolate our network anyways. 6 | load-module module-native-protocol-unix auth-anonymous=1 socket=/tmp/pulse-socket 7 | load-module module-native-protocol-tcp auth-anonymous=1 -------------------------------------------------------------------------------- /js/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /js/docker/nginx.Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 0, "build-stage", based on Node.js, to build and compile the frontend 2 | # Build stage 3 | FROM node:20-alpine as build-stage 4 | 5 | WORKDIR /app 6 | 7 | COPY package*.json ./ 8 | 9 | RUN npm install 10 | 11 | COPY . . 12 | 13 | RUN npm run build 14 | 15 | # Production stage 16 | FROM nginx:1.21-alpine as production-stage 17 | 18 | COPY --from=build-stage /app/build /usr/share/nginx/html 19 | 20 | EXPOSE 80 21 | 22 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /js/firebase_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": "AIzaSyApOjVnuRBNO7x5UP-Cq7VSzcylCMjDBng", 3 | "authDomain": "android-emulator-webrtc-demo.firebaseapp.com", 4 | "databaseURL": "https://android-emulator-webrtc-demo.firebaseio.com", 5 | "projectId": "android-emulator-webrtc-demo", 6 | "storageBucket": "android-emulator-webrtc-demo.appspot.com", 7 | "messagingSenderId": "431559312508", 8 | "appId": "1:431559312508:web:c9aaa2a9ac2f48657e35f9", 9 | "measurementId": "G-MTCT6N00FV" 10 | } -------------------------------------------------------------------------------- /emu/templates/cloudbuild.README.MD: -------------------------------------------------------------------------------- 1 | Emulator version: {{emu_version}} 2 | ================================= 3 | 4 | ## Release Notes 5 | 6 | This contains the release notes that accompanies the `cloudbuild.yaml` file. 7 | 8 | The yaml file will build: 9 | 10 | Emulator version: {{emu_version}}: 11 | 12 | {{emu_images}} 13 | 14 | 15 | ## Building Locally 16 | 17 | Follow the instructions [here](https://cloud.google.com/cloud-build/docs/build-debug-locally) if 18 | you wish to build all the images locally. 19 | -------------------------------------------------------------------------------- /js/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | 5 | import App from './App'; 6 | import * as serviceWorker from './serviceWorker'; 7 | 8 | ReactDOM.render(, document.getElementById('root')); 9 | 10 | // If you want your app to work offline and load faster, you can change 11 | // unregister() to register() below. Note this comes with some pitfalls. 12 | // Learn more about service workers: https://bit.ly/CRA-PWA 13 | serviceWorker.unregister(); 14 | -------------------------------------------------------------------------------- /js/.dockerignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # grpc/protoc related things. 15 | /grpc-web 16 | /protoc-plugin 17 | /android_emulation_control 18 | 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | protoc-gen-grpc-web 30 | -------------------------------------------------------------------------------- /js/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | *.o 8 | 9 | # testing 10 | coverage 11 | 12 | # production 13 | build 14 | 15 | # yalc 16 | .yalc 17 | yalc.lock 18 | 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | protoc-gen-grpc-web 30 | src/android_emulation_control/* 31 | docker/certs/jwt_secrets_pub.jwks 32 | src/config.js -------------------------------------------------------------------------------- /js/src/components/copyright.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Description of this file. 3 | */ 4 | import React, { Component } from "react"; 5 | import Typography from "@mui/material/Typography"; 6 | import Link from "@mui/material/Link"; 7 | 8 | export default class Copyright extends Component { 9 | render() { 10 | return ( 11 | 12 | {"Copyright © "} 13 | 14 | Your Website 15 | {" "} 16 | {new Date().getFullYear()} 17 | {"."} 18 | 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /emu/templates/avd/Pixel2.ini: -------------------------------------------------------------------------------- 1 | # Copyright 2019 - The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | # Basic config used to create an avd for now. 17 | path=/android-home/Pixel2.avd -------------------------------------------------------------------------------- /js/develop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | trap killgroup SIGINT 3 | 4 | killgroup(){ 5 | echo killing... be patient... 6 | docker stop emu-dev-grpc-web 7 | kill 0 8 | } 9 | 10 | docker build -t emu-dev-web -f develop/envoy.Dockerfile develop 11 | 12 | cd "$(dirname "$0")" 13 | BUILD_OS=$(uname -s) 14 | case $BUILD_OS in 15 | Darwin) 16 | echo "Building for Mac" 17 | docker run --rm -p 8080:8080 -p 8001:8001 --name emu-dev-grpc-web emu-dev-web & 18 | ;; 19 | *) 20 | echo "Building for linux" 21 | docker run --rm -p 8080:8080 -p 8001:8001 --name emu-dev-grpc-web "--add-host=host.docker.internal:host-gateway" emu-dev-web & 22 | ;; 23 | esac 24 | 25 | echo "Make sure you are running the android emulator & video bridge" 26 | echo "Launch the video bridge with 'goldfish-webrtc-bridge --port 9554'" 27 | 28 | npm start 29 | -------------------------------------------------------------------------------- /js/docker/envoy.Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | #FROM envoyproxy/envoy:latest 15 | 16 | FROM envoyproxy/envoy:v1.26-latest 17 | COPY ./envoy.yaml /etc/envoy/envoy.yaml 18 | EXPOSE 8080 19 | RUN chmod go+r /etc/envoy/envoy.yaml 20 | CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml 21 | -------------------------------------------------------------------------------- /js/develop/envoy.Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | FROM envoyproxy/envoy:v1.26-latest 15 | 16 | # Workaround for linux missing host.docker.internal 17 | COPY ./envoy.yaml /etc/envoy/envoy.yaml 18 | EXPOSE 8080 19 | RUN chmod go+r /etc/envoy/envoy.yaml 20 | CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml 21 | -------------------------------------------------------------------------------- /js/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | .container { 27 | width: 100%; 28 | height: 768px; 29 | margin: auto; 30 | padding: 10px; 31 | } 32 | 33 | .leftpanel { 34 | float: left; 35 | height:768px; 36 | width: 440px; 37 | } 38 | .rightpanel { 39 | margin-left: 15%; 40 | height:768px; 41 | } 42 | @keyframes App-logo-spin { 43 | from { 44 | transform: rotate(0deg); 45 | } 46 | to { 47 | transform: rotate(360deg); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "emu-docker" 3 | version = "0.1.0" 4 | description = "A script that enables you to create a docker container with a running android emulator" 5 | authors = ["Erwin Jansen "] 6 | license = "Apache-2.0" 7 | readme = "README.md" 8 | packages = [{ include = "emu" }] 9 | 10 | [tool.poetry.scripts] 11 | emu-docker = "emu.emu_docker:main" 12 | 13 | [tool.poetry.dependencies] 14 | python = "^3.10" 15 | PyYAML = "^6.0" 16 | docker = "^7.1.0" 17 | tqdm = "^4.65.0" 18 | console-menu = "^0.8.0" 19 | click = "^8.1.3" 20 | appdirs = "^1.4.4" 21 | colorlog = "^6.7.0" 22 | Jinja2 = "^3.1.2" 23 | 24 | [tool.poetry.group.dev.dependencies] 25 | check-manifest = "^0.49" 26 | versioneer = "^0.28" 27 | black = "^23.3.0" 28 | coverage = "^7.2.5" 29 | mock = "^5.0.2" 30 | tox = "^4.5.1" 31 | 32 | [build-system] 33 | requires = ["poetry-core"] 34 | build-backend = "poetry.core.masonry.api" 35 | -------------------------------------------------------------------------------- /js/docker/emulator.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Emulator adb service with docker compose 3 | Requires=docker.service 4 | After=docker.service 5 | 6 | [Service] 7 | Restart=always 8 | WorkingDirectory=/opt/emulator/ 9 | 10 | # Remove old containers, images and volumes 11 | ExecStartPre=/usr/local/bin/docker-compose down -v 12 | ExecStartPre=/usr/local/bin/docker-compose rm -fv 13 | ExecStartPre=-/bin/bash -c 'docker volume ls -qf "name=emulator_" | xargs docker volume rm' 14 | ExecStartPre=-/bin/bash -c 'docker network ls -qf "name=emulator_" | xargs docker network rm' 15 | ExecStartPre=-/bin/bash -c 'docker ps -aqf "name=emulator_*" | xargs docker rm' 16 | # Compose up 17 | ExecStart=/usr/local/bin/docker-compose up 18 | ExecStartPost=/bin/bash -c 'echo "VIRTUAL_DEVICE_BOOT_STARTED"' 19 | 20 | # Compose down, remove containers and volumes 21 | ExecStop=/usr/local/bin/docker-compose down -v 22 | 23 | [Install] 24 | WantedBy=multi-user.target 25 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.11.0", 7 | "@emotion/styled": "^11.11.0", 8 | "@mui/icons-material": "^5.11.16", 9 | "@mui/lab": "^5.0.0-alpha.130", 10 | "@mui/material": "^5.13.1", 11 | "@mui/styles": "^5.13.1", 12 | "@react-firebase/auth": "^0.2.10", 13 | "android-emulator-webrtc": "^1.0.18", 14 | "axios": "^1.4.0", 15 | "firebase": "^7.19.0", 16 | "react": "^17.0.2", 17 | "react-dom": "^17.0.2", 18 | "react-scripts": "^5.0.1" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | 28 | }, 29 | "browserslist": [ 30 | ">0.2%", 31 | "not dead", 32 | "not ie <= 11", 33 | "not op_mini all" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | CONTAINER_ID=$1 15 | shift 16 | PARAMS="$@" 17 | docker run \ 18 | --device /dev/kvm \ 19 | --publish 8554:8554/tcp \ 20 | --publish 5554:5554/tcp \ 21 | --publish 5555:5555/tcp \ 22 | -e TOKEN="$(cat ~/.emulator_console_auth_token)" \ 23 | -e ADBKEY="$(cat ~/.android/adbkey)" \ 24 | -e TURN \ 25 | -e EMULATOR_PARAMS="${PARAMS}" \ 26 | ${CONTAINER_ID} 27 | -------------------------------------------------------------------------------- /js/docker/docker-compose-build.yaml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | front-envoy: 4 | image: emulator_envoy:latest 5 | build: 6 | context: . 7 | dockerfile: envoy.Dockerfile 8 | networks: 9 | - envoymesh 10 | expose: 11 | - "8080" 12 | - "8001" 13 | - "8443" 14 | ports: 15 | - "80:8080" 16 | - "443:8443" 17 | - "8001:8001" 18 | - "8080:8080" 19 | emulator: 20 | image: emulator_emulator:latest 21 | build: 22 | context: ../../bld/emulator 23 | dockerfile: Dockerfile 24 | networks: 25 | envoymesh: 26 | aliases: 27 | - emulator 28 | devices: [/dev/kvm] 29 | shm_size: 128M 30 | expose: 31 | - "8554" 32 | nginx: 33 | image: emulator_nginx:latest 34 | build: 35 | context: .. 36 | dockerfile: docker/nginx.Dockerfile 37 | networks: 38 | envoymesh: 39 | aliases: 40 | - nginx 41 | expose: 42 | - "80" 43 | 44 | networks: 45 | envoymesh: {} 46 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # content of: tox.ini , put in same dir as setup.py 2 | # tox.ini 3 | [tox] 4 | isolated_build = True 5 | 6 | [tox:.package] 7 | # note tox will use the same python version as under what tox is installed to package 8 | # so unless this is python 3 you can require a given python version for the packaging 9 | # environment via the basepython key 10 | basepython = python3 11 | 12 | [testenv] 13 | # install pytest in the virtualenv where commands will be executed 14 | deps = pytest 15 | platform = linux|darwin 16 | commands = 17 | # NOTE: you can run any command line tool here - not just tests 18 | pytest 19 | 20 | [pytest] 21 | norecursedirs = docs *.egg-info .git src js venv py2 py.tox 22 | log_format = %(asctime)s {%(pathname)s:%(lineno)d} %(levelname)s %(message)s 23 | log_date_format = %Y-%m-%d %H:%M:%S 24 | log_level = INFO 25 | # log_cli = True 26 | markers = 27 | slow: marks tests as slow (deselect with '-m "not slow"') 28 | e2e: marks test as end to end, requires docker (deselect with '-m "not e2e"') 29 | linux: marks test that require kvm & docker (deselect with '-m "not linux"') 30 | -------------------------------------------------------------------------------- /js/docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | front-envoy: 4 | image: emulator_envoy:latest 5 | container_name: emulator_envoy 6 | networks: 7 | - envoymesh 8 | expose: 9 | - "8080" 10 | - "8001" 11 | ports: 12 | - "80:8080" 13 | - "443:8080" 14 | - "8001:8001" 15 | - "8080:8080" 16 | emulator: 17 | image: emulator_emulator:latest 18 | container_name: emulator_emulator 19 | networks: 20 | envoymesh: 21 | aliases: 22 | - emulator 23 | devices: [/dev/kvm] 24 | shm_size: 128M 25 | expose: 26 | - "8554" 27 | #jwt_signer: 28 | # image: emulator_jwt_signer:latest 29 | # container_name: emulator_jwt_signer 30 | # networks: 31 | # envoymesh: 32 | # aliases: 33 | # - jwt_signer 34 | # expose: 35 | # - "8080" 36 | 37 | nginx: 38 | image: emulator_nginx:latest 39 | # network_mode: "host" 40 | container_name: emulator_nginx 41 | networks: 42 | envoymesh: 43 | aliases: 44 | - nginx 45 | expose: 46 | - "80" 47 | 48 | networks: 49 | envoymesh: {} 50 | -------------------------------------------------------------------------------- /tests/test_template_writer.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import pytest 15 | 16 | from emu.template_writer import TemplateWriter 17 | 18 | 19 | def test_writer_writes_file(temp_dir): 20 | writer = TemplateWriter(temp_dir) 21 | writer.write_template("cloudbuild.README.MD", {}) 22 | assert (temp_dir / "cloudbuild.README.MD").exists() 23 | 24 | 25 | def test_renames_file(temp_dir): 26 | writer = TemplateWriter(temp_dir) 27 | writer.write_template("cloudbuild.README.MD", {}, "foo") 28 | assert (temp_dir / "foo").exists() 29 | -------------------------------------------------------------------------------- /js/config_gen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import json 4 | 5 | 6 | def replace(fname, search, replace): 7 | if search == replace: 8 | return 9 | 10 | txt = "" 11 | with open(fname, "r") as inp: 12 | txt = inp.read() 13 | txt.replace(search, replace) 14 | with open(fname, "w") as out: 15 | out.write(txt) 16 | 17 | 18 | def main(): 19 | if len(sys.argv) != 2: 20 | sys.exit(1) 21 | 22 | config = {} 23 | with open(sys.argv[1], "r") as cfg: 24 | config = json.load(cfg) 25 | 26 | with open("src/config.js", "w") as cfg: 27 | lines = [ 28 | "// Do not edit this file!", 29 | "// This file was autogenerated by running:", 30 | "// {}".format(" ".join(sys.argv)), 31 | "export const config = {};".format(json.dumps(config, sort_keys=True, indent=4)), 32 | ] 33 | cfg.write("\n".join(lines)) 34 | 35 | replace("develop/envoy.yaml", "android-emulator-webrtc-demo", config["projectId"]) 36 | replace("docker/envoy.yaml", "android-emulator-webrtc-demo", config["projectId"]) 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows 28 | [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /emu/templates/avd/Pixel2.avd/config.ini: -------------------------------------------------------------------------------- 1 | AvdId=Pixel2 2 | PlayStore.enabled={{playstore}} 3 | avd.ini.displayname=Pixel2 4 | avd.ini.encoding=UTF-8 5 | # Real Pixel2 ships with 32GB 6 | disk.dataPartition.size=512MB 7 | fastboot.forceColdBoot=no 8 | hw.accelerometer=yes 9 | hw.audioInput=yes 10 | hw.battery=yes 11 | hw.camera.back=emulated 12 | hw.camera.front=emulated 13 | hw.cpu.ncore=4 14 | hw.dPad=no 15 | hw.device.hash2=MD5:bc5032b2a871da511332401af3ac6bb0 16 | hw.device.manufacturer=Google 17 | hw.gps=yes 18 | hw.gpu.enabled=yes 19 | hw.gpu.mode=auto 20 | hw.initialOrientation=Portrait 21 | hw.keyboard=yes 22 | hw.mainKeys=no 23 | hw.ramSize=4096 24 | hw.sensors.orientation=yes 25 | hw.sensors.proximity=yes 26 | hw.trackBall=no 27 | runtime.network.latency=none 28 | runtime.network.speed=full 29 | vm.heapSize=512 30 | tag.display=Google APIs 31 | # Note: these are the ones that usually vary from device to device 32 | # for example a Pixel 6 will use 420x2400x1080 33 | hw.lcd.density=440 34 | hw.lcd.height=1920 35 | hw.lcd.width=1080 36 | # Unused 37 | # hw.sdCard=yes 38 | # sdcard.size=512M 39 | 40 | tag.id={{qemu_tag}} 41 | abi.type={{ro_product_cpu_abi}} 42 | hw.cpu.arch={{qemu_cpu}} 43 | image.sysdir.1=system-images/android/{{ro_product_cpu_abi}}/ 44 | 45 | # End of default configuration 46 | 47 | -------------------------------------------------------------------------------- /run-with-gpu.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # This launcher will force the emulator to use hardware acceleration. In order to use this you will need to have 16 | # installed the nvida docker container drivers (https://github.com/NVIDIA/nvidia-docker) 17 | CONTAINER_ID=$1 18 | shift 19 | PARAMS="$@" 20 | # Allow display access from the container. 21 | xhost +si:localuser:root 22 | docker run --gpus all \ 23 | --device /dev/kvm \ 24 | --publish 8554:8554/tcp \ 25 | --publish 5555:5555/tcp \ 26 | -v /tmp/.X11-unix:/tmp/.X11-unix \ 27 | -e DISPLAY \ 28 | -e TOKEN="$(cat ~/.emulator_console_auth_token)" \ 29 | -e ADBKEY="$(cat ~/.android/adbkey)" \ 30 | -e EMULATOR_PARAMS="-gpu host ${PARAMS}" \ 31 | ${CONTAINER_ID} 32 | -------------------------------------------------------------------------------- /tests/test_platform_tools.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import unittest.mock as mock 16 | 17 | import pytest 18 | 19 | from emu.platform_tools import PlatformTools 20 | 21 | 22 | @pytest.fixture 23 | def mock_zip(): 24 | with mock.patch("zipfile.ZipFile") as mock_zip: 25 | yield mock_zip 26 | 27 | 28 | @pytest.fixture 29 | def platform_tools(mock_zip): 30 | return PlatformTools("/tmp/tools.zip") 31 | 32 | 33 | def test_extract_unzips_something(mock_zip, platform_tools): 34 | """Unzips adb""" 35 | platform_tools.extract_adb("foo") 36 | 37 | mock_zip.assert_called_with("/tmp/tools.zip", "r") 38 | zip_handle = mock_zip.return_value.__enter__.return_value 39 | zip_handle.extract.assert_called_with("platform-tools/adb", "foo") 40 | -------------------------------------------------------------------------------- /js/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) 17 | CURRENT_DIR := $(abspath $(MAKEFILE_PATH)/..) 18 | 19 | .PHONY: build-release run-release develop stop 20 | 21 | all: develop 22 | 23 | 24 | deps: src/config.js 25 | @npm install 26 | 27 | 28 | # This transforms your firebase_config.json into the settings file 29 | # and fixes up you envoy settings to enable token validation. 30 | src/config.js: 31 | python config_gen.py firebase_config.json 32 | 33 | 34 | develop: 35 | ./develop.sh 36 | 37 | build-release: deps 38 | docker-compose -f docker/docker-compose.yaml build 39 | 40 | run-release: build-release 41 | docker-compose -f docker/docker-compose.yaml up 42 | 43 | stop: 44 | docker stop emu-dev-grpc-web; pkill -9 npm; pkill -9 js; pkill -9 node; true 45 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import pytest 15 | import docker 16 | import tempfile 17 | import shutil 18 | from pathlib import Path 19 | 20 | @pytest.fixture 21 | def client() -> docker.DockerClient: 22 | assert docker.from_env().ping() 23 | yield docker.from_env() 24 | 25 | 26 | @pytest.fixture 27 | def clean_docker(client): 28 | # Remove all containers 29 | containers = client.containers.list(all=True) 30 | for container in containers: 31 | container.remove(force=True) 32 | 33 | # Remove all images 34 | images = client.images.list(all=True) 35 | for image in images: 36 | client.images.remove(image.id, force=True) 37 | 38 | yield client 39 | 40 | @pytest.fixture() 41 | def temp_dir(): 42 | """Creates a temporary directory that gets deleted after the test.""" 43 | temp_directory = tempfile.mkdtemp() 44 | yield Path(temp_directory) 45 | shutil.rmtree(temp_directory) -------------------------------------------------------------------------------- /configure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright 2019 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | if [ "${BASH_SOURCE-}" = "$0" ]; then 16 | echo "You must source this script: \$ source $0" >&2 17 | echo "It will create a virtual environment in which emu-docker will be installed." 18 | exit 33 19 | fi 20 | 21 | PYTHON=python3 22 | VENV=.venv 23 | 24 | if [ ! -f "./$VENV/bin/activate" ]; then 25 | # Prefer python3 if it is available. 26 | if command -v python3 &>/dev/null; then 27 | echo "Using python 3" 28 | $PYTHON -m venv $VENV 29 | [ -e ./$VENV/bin/pip ] && ./$VENV/bin/pip install --upgrade pip 30 | [ -e ./$VENV/bin/pip ] && ./$VENV/bin/pip install --upgrade setuptools 31 | else 32 | echo "Using python 2 ----<< Deprecated! See: https://python3statement.org/.." 33 | echo "You need to upgrade to python3" 34 | exit 33 35 | fi 36 | fi 37 | if [ -e ./$VENV/bin/activate ]; then 38 | . ./$VENV/bin/activate 39 | pip install -e . 40 | echo "Ready to run emu-docker!" 41 | fi 42 | -------------------------------------------------------------------------------- /run-in-script-example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This is the remote image we are going to run. 4 | # Docker will obtain it for us if needed. 5 | DOCKER_IMAGE=us-docker.pkg.dev/android-emulator-268719/images/r-google-x64:30.0.23 6 | 7 | # This is the forwarding port. Higher ports are preferred as to not interfere 8 | # with adb's ability to scan for emulators. 9 | PORT=15555 10 | 11 | # This will launch the container in the background. 12 | container_id=$(docker run -d \ 13 | --device /dev/kvm \ 14 | --publish 8554:8554/tcp \ 15 | --publish $PORT:5555/tcp \ 16 | -e TOKEN="$(cat ~/.emulator_console_auth_token)" \ 17 | -e ADBKEY="$(cat ~/.android/adbkey)" \ 18 | -e EMULATOR_PARAMS="${PARAMS}" \ 19 | $DOCKER_IMAGE) 20 | 21 | echo "The container is running with id: $container_id" 22 | 23 | # Note you might see something like: 24 | # failed to connect to localhost:15555 25 | # this merely indicates that the container is not yet ready. 26 | 27 | echo "Connecting to forwarded adb port." 28 | adb connect localhost:$PORT 29 | 30 | # we basically have to wait until `docker ps` shows us as healthy. 31 | # this can take a bit as the emulator needs to boot up! 32 | echo "Waiting until the device is ready" 33 | adb wait-for-device 34 | 35 | # The device is now booting, or close to be booted 36 | # We just wait until the sys.boot_completed property is set to 1. 37 | while [ "$(adb shell getprop sys.boot_completed | tr -d '\r')" != "1" ]; do 38 | echo "Still waiting for boot.." 39 | sleep 1 40 | done 41 | 42 | echo "The device is ready" 43 | echo "Run the following command to stop the container:" 44 | echo "docker stop ${container_id}" 45 | -------------------------------------------------------------------------------- /emu/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import logging 15 | from pathlib import Path 16 | 17 | import requests 18 | from tqdm import tqdm 19 | 20 | 21 | def download(url, dest : Path) -> Path: 22 | """Downloads the given url to the given destination with a progress bar. 23 | 24 | This function will immediately return if the file already exists. 25 | """ 26 | dest = Path(dest) 27 | if dest.exists(): 28 | print(f" Skipping already downloaded file: {dest}") 29 | return dest 30 | 31 | # Make sure destination directory exists. 32 | if not dest.parent.exists(): 33 | dest.parent.mkdir(parents=True) 34 | 35 | logging.info("Get %s -> %s", url, dest) 36 | with requests.get(url, timeout=5, stream=True) as r: 37 | with tqdm(r, total=int(r.headers["content-length"]), unit="B", unit_scale=True) as t: 38 | with open(dest, "wb") as f: 39 | for data in r: 40 | f.write(data) 41 | t.update(len(data)) 42 | return dest 43 | -------------------------------------------------------------------------------- /tests/e2e/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import os 15 | import shutil 16 | import socket 17 | import tempfile 18 | from contextlib import closing 19 | from distutils.spawn import find_executable 20 | 21 | 22 | def find_free_port(): 23 | with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: 24 | s.bind(("localhost", 0)) 25 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 26 | return s.getsockname()[1] 27 | 28 | 29 | def find_adb(): 30 | adb_loc = os.path.join(os.environ.get("ANDROID_SDK_ROOT", ""), "platform-tools", "adb") 31 | if not (os.path.exists(adb_loc) and os.access(adb_loc, os.X_OK)): 32 | adb_loc = find_executable("adb") 33 | return adb_loc 34 | 35 | 36 | class TempDir(object): 37 | """Creates a temporary directory that automatically is deleted.""" 38 | 39 | def __enter__(self): 40 | self.tmpdir = tempfile.mkdtemp("unittest") 41 | return self.tmpdir 42 | 43 | def __exit__(self, exc_type, exc_value, tb): 44 | shutil.rmtree(self.tmpdir) 45 | -------------------------------------------------------------------------------- /emu/platform_tools.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import zipfile 15 | from pathlib import Path 16 | 17 | from emu.utils import download 18 | 19 | 20 | class PlatformTools(object): 21 | """The platform tools zip file. It will be downloaded on demand.""" 22 | 23 | # Platform tools, needed to get adb. 24 | PLATFORM_TOOLS_URL = ( 25 | "https://dl.google.com/android/repository/platform-tools_r29.0.5-linux.zip" 26 | ) 27 | PLATFORM_TOOLS_ZIP = "platform-tools-latest-linux.zip" 28 | 29 | def __init__(self, fname=None): 30 | self.platform = fname 31 | 32 | def extract_adb(self, dest): 33 | if not self.platform: 34 | self.platform = self.download() 35 | with zipfile.ZipFile(self.platform, "r") as plzip: 36 | plzip.extract("platform-tools/adb", dest) 37 | 38 | def download(self, dest=None): 39 | """Downloads the platform tools zip to the given destination""" 40 | dest = dest or Path.cwd() / PlatformTools.PLATFORM_TOOLS_ZIP 41 | print(f"Downloading platform tools to {dest}") 42 | return download(PlatformTools.PLATFORM_TOOLS_URL, dest) 43 | -------------------------------------------------------------------------------- /js/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /js/docker/certs/self_sign.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFNDCCAxwCCQDC3TN4R3jFzTANBgkqhkiG9w0BAQsFADBcMQswCQYDVQQGEwJV 3 | UzEPMA0GA1UECAwGRGVuaWFsMRQwEgYDVQQHDAtTcHJpbmdmaWVsZDEMMAoGA1UE 4 | CgwDRGlzMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20wHhcNMTkwNTEzMTc1MzIy 5 | WhcNMjAwNTEyMTc1MzIyWjBcMQswCQYDVQQGEwJVUzEPMA0GA1UECAwGRGVuaWFs 6 | MRQwEgYDVQQHDAtTcHJpbmdmaWVsZDEMMAoGA1UECgwDRGlzMRgwFgYDVQQDDA93 7 | d3cuZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC4 8 | Wc+6VSUS8rJ6mqP4zqmyXDAvxLHdwzw56yFrCfErmDN/WKjPp2MKzFKA4OEw2UOW 9 | UNl07hudDV6OSiiJS1A1JmJZuwvxoX1m7INNHM53cqzFno2s0e6IXfcp3tFpIW2A 10 | jL0qsppcCe8yjA0Ei3mwS1hM8QuiVH7+VBemuU2iphBJ9jCLok//V/V1eXICf24G 11 | xTAovC+wJdjRkx/0NYhUzMLAx9TrYtwgXsRPI3mEIO1WbOGIVruvZJ+RomVnB6v+ 12 | 1nIJtYLOBBvgUp35uiLZtY/629uJxNJOStVKcpXay0cLpFZHCe1ZUrzeH4O/liyc 13 | RKSFiJ+VImmhE9lBJDpWNVR1OjQXoY0T/lsDcaDbOlwElq1d+5UxGaJTmcnnoT89 14 | TmbxxXGeQ1glu9ZtH3PK7XHecEAvpE+GnyfQ742iNUKNvr8wF4/uraP6eJMSQXFc 15 | 6cR79CtsgM55DlN1xz/4laPT8M+42KLaTDIv+mFljYXp+ChXqqm4ZlgCAvH/PmxQ 16 | 5RzUID5rOnwG1/cZynPpjA9ylrJka51FQjvjr5WEDzR3YWeDzDJ6w2g6zFob5afC 17 | lA0k2RJM8QDgA77cp6pPOT0p0eYEJPuiXRtAsHZt/Mah4MH1BT3tc8nhwowmw8PI 18 | lP2dQywiy6+PtsXHalgtfBu2dvPTPoZ5I7tBYHvn8wIDAQABMA0GCSqGSIb3DQEB 19 | CwUAA4ICAQCJVqJ9p0UUQ3OO9Jr01IfQr/6c108l7xbaWlWCr7bwc8pG/pP1jnOU 20 | c0D/D7+uGccStNRg5ksM/Nv/JYUqVLADqt4G9jtBa057n/Z2Qf4fVuLMbf1GlRTw 21 | IEJjYnPgAkmaDbQaIhSbNlxHQa1ZAhJT2IZXj5ZBdwpfUGcSMGIOmuHupg/tkjVi 22 | A5a6uDcaQmiwGJUUG0D36RBtvDIDM171vfpxF23/l+orxW6zGsfzcAHdx5wuQbjZ 23 | hvlDLsA1lgMjVkHBGqWxRJZHw//5idwaKdSy53rxcCReXYsZVNBZ4Rdp1ILGeNHM 24 | YB0WWvt1yZajORiwmwftd+jjoIBioQf7rZR09Ao5H+yjcNQtj3HhmfdGNJOz/0wb 25 | kQ1EdIJeHgPySTKRgLcIia4qkk6ehx7jJ/gNxJ5WOXcdBcU77LqehG+brz5PDJF6 26 | lCaVG4IFNUIZA9tlE31eWOOg5u9+hqN07nCSwOnywD6TWU/1IWInyJss5XOo4u7f 27 | 0MqXFkXphdIghaLFmcAXASUJi0E8/qJcAIE8DMH11ToZcBLeIvvt6lhkbHT1oE74 28 | CU255svOgjC8kaNvNew3gwDyMv9GxoddSRpKspHnBbo9gl6Rj4abfijz9glokG7T 29 | H++Bv1dW3FqUEAjF2hhaBgMGvFGvf+ETLH4TJWbwTkPb9ml4bFl0Jw== 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /tests/e2e/test_docker_container.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Test that some remote containers are available.""" 15 | import pytest 16 | from emu.emu_downloads_menu import find_image 17 | from emu.containers.system_image_container import SystemImageContainer 18 | 19 | 20 | @pytest.mark.e2e 21 | @pytest.mark.parametrize( 22 | "image", 23 | [ 24 | "P google_apis x86_64", 25 | "P android x86_64", 26 | # "Q google_apis x86_64", 27 | "Q android x86_64", 28 | "R google_apis x86_64", 29 | # "R android x86_64", 30 | ], 31 | ) 32 | def test_can_pull(image): 33 | """Make sure we have this set of images publicly available.""" 34 | info = find_image(image)[0] 35 | assert info 36 | 37 | container = SystemImageContainer(info) 38 | assert container.can_pull() 39 | 40 | 41 | @pytest.mark.e2e 42 | @pytest.mark.parametrize( 43 | "image", 44 | [ 45 | "K android x86", 46 | ], 47 | ) 48 | def test_can_build(image, temp_dir): 49 | """Make sure we can build images that are not available.""" 50 | info = find_image(image)[0] 51 | assert info 52 | 53 | # We should not be hosting this image 54 | container = SystemImageContainer(info) 55 | assert not container.can_pull() 56 | 57 | # But we should be able to build it 58 | assert container.build(temp_dir) 59 | 60 | # And now it should be available locally 61 | assert container.available() 62 | 63 | 64 | -------------------------------------------------------------------------------- /js/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { TokenProviderService } from "./service/auth_service"; 3 | import EmulatorScreen from "./components/emulator_screen"; 4 | import LoginPage from "./components/login_firebase"; 5 | import { ThemeProvider, makeStyles } from '@mui/styles'; 6 | import { createTheme } from '@mui/material/styles'; 7 | 8 | import "./App.css"; 9 | 10 | const development = 11 | !process.env.NODE_ENV || process.env.NODE_ENV === "development"; 12 | 13 | var EMULATOR_GRPC = 14 | window.location.protocol + 15 | "//" + 16 | window.location.hostname + 17 | ":" + 18 | // For some reason, process.env.NODE_ENV either doesn't exist or is equal to "development", causing EMULATOR_GRPC to equal/point to "localhost:" verus "localhost:8080" which is where Envoy is listening for gRPC-Web requests. Hard-coding the appendation of ":8080" as we've done here restores some functionality, as now both the WebRTC and PNG views return a status of "connecting" and the JavaScript console no longer logs any 'JWT is missing' errors. However, the WebRTC and PNG views never return a status of "connected" so video and audio of the emulator cannot be seen/heard. 19 | "8080"; 20 | // window.location.port; 21 | if (development) { 22 | EMULATOR_GRPC = window.location.protocol + "//" + 23 | window.location.hostname + ":8080"; 24 | } 25 | 26 | 27 | console.log(`Connecting to grpc at ${EMULATOR_GRPC}`); 28 | 29 | const useStyles = makeStyles((theme) => ({ 30 | root: { 31 | // some CSS that accesses the theme 32 | } 33 | })); 34 | 35 | const theme = createTheme({ 36 | 37 | }); 38 | 39 | const auth = new TokenProviderService(); 40 | 41 | export default function App() { 42 | const [authorized, setAuthorized] = useState(false); 43 | 44 | useEffect(() => { 45 | const handleAuthorization = (a) => { 46 | setAuthorized(a); 47 | }; 48 | 49 | auth.on("authorized", handleAuthorization); 50 | }, []); 51 | 52 | 53 | return ( 54 | 55 | {authorized ? ( 56 | 57 | ) : ( 58 | 59 | )} 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /tests/e2e/test_cloud_build.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """End 2 end test that builds every image. 15 | 16 | This is super expensive.. 17 | """ 18 | import collections 19 | import os 20 | import shutil 21 | import subprocess 22 | 23 | import docker 24 | import pytest 25 | from utils import TempDir 26 | 27 | from emu.cloud_build import cloud_build 28 | 29 | Arguments = collections.namedtuple("Args", "emuzip, img, dest, repo, git, sys") 30 | 31 | 32 | @pytest.mark.slow 33 | @pytest.mark.e2e 34 | def test_build_cloud_only_emu(temp_dir): 35 | assert docker.from_env().ping() 36 | # Make sure we accept all licenses, 37 | args = Arguments( 38 | "canary", "Q google_apis x86_64", temp_dir, "us-docker.pkg.dev/android-emulator-268719/images", False, False 39 | ) 40 | cloud_build(args) 41 | expected_files = [ 42 | "cloudbuild.yaml", 43 | "README.MD", 44 | "29-google-x64", 45 | "29-google-x64-no-metrics", 46 | ] 47 | for file_name in expected_files: 48 | assert (temp_dir / file_name).exists() 49 | 50 | 51 | @pytest.mark.slow 52 | @pytest.mark.e2e 53 | def test_build_cloud_only_sys(temp_dir): 54 | assert docker.from_env().ping() 55 | # Make sure we accept all licenses, 56 | args = Arguments( 57 | "canary", "Q google_apis x86_64", temp_dir, "us-docker.pkg.dev/android-emulator-268719/images", False, True 58 | ) 59 | cloud_build(args) 60 | expected_files = [ 61 | "cloudbuild.yaml", 62 | "README.MD", 63 | "sys-29-google-x64", 64 | ] 65 | for file_name in expected_files: 66 | assert (temp_dir / file_name).exists() 67 | -------------------------------------------------------------------------------- /js/turn/turn.py: -------------------------------------------------------------------------------- 1 | # Lint as: python3 2 | """A basic example of a service that can hand out temporary access to a turn server. 3 | 4 | It follows the rest api for access to turn services: 5 | 6 | https://tools.ietf.org/html/draft-uberti-rtcweb-turn-rest-00 7 | """ 8 | 9 | import base64 10 | import hmac 11 | import hashlib 12 | import time 13 | 14 | from absl import app, flags 15 | from flask import Flask, request, abort 16 | 17 | FLAGS = flags.FLAGS 18 | flags.DEFINE_string("static_auth_secret", "supersecret", "The shared secret with the turn server.") 19 | flags.DEFINE_string("api_key", "supersafe", "The api key the emulator will present to retrieve turn configuration.") 20 | flags.DEFINE_integer("port", 8080, "The port where this service will run") 21 | 22 | api = Flask(__name__) 23 | 24 | 25 | @api.route("/turn/", methods=["GET"]) 26 | def get_turn_cfg(iceserver): 27 | """Generates a turn configuration that can be used by the emulator. 28 | The turn configuration is valid for 1 hour. 29 | 30 | See: https://tools.ietf.org/html/draft-uberti-rtcweb-turn-rest-00 31 | 32 | Your turn server should be configured to use the same static auth secret as 33 | the turn server 34 | 35 | For example: 36 | 37 | turnserver -n -v --log-file=stdout --static-auth-secret=supersecret \ 38 | -r localhost -use-auth-secret 39 | 40 | python turn.py --static_auth_secret supersecret 41 | 42 | Args: 43 | iceserver: The turn server under your control that uses the 44 | shared secret. 45 | """ 46 | if request.args.get("apiKey") != FLAGS.api_key: 47 | abort(403, description="Invalid api key") 48 | 49 | epoch = int(time.time()) + 3600 # You get an hour. 50 | userid = "{}:{}".format(epoch, "someone") # Username doesn't matter. 51 | key = bytes(FLAGS.static_auth_secret, "UTF-8") # Shared secret with coturn config 52 | mac = hmac.digest(key, bytes(userid, "UTF-8"), hashlib.sha1) 53 | credential = base64.b64encode(mac).decode() 54 | return {"iceServers": [{"urls": ["turn:{}".format(iceserver)], "username": userid, "credential": credential}]} 55 | 56 | 57 | def main(argv): 58 | if len(argv) > 1: 59 | raise app.UsageError("Too many command-line arguments.") 60 | api.run(host="0.0.0.0", port=FLAGS.port) 61 | 62 | 63 | if __name__ == "__main__": 64 | app.run(main) 65 | -------------------------------------------------------------------------------- /emu/templates/Dockerfile.system_image: -------------------------------------------------------------------------------- 1 | # Copyright 2021 - The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2_0 (the "License"); 4 | # you may not use this file except in compliance with the License_ 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www_apache_org/licenses/LICENSE-2_0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied_ 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License_ 14 | FROM alpine:3.18 AS unzipper 15 | RUN apk add --update unzip 16 | 17 | # Barely changes 18 | FROM unzipper as sys_unzipper 19 | COPY {{system_image_zip}} /tmp/ 20 | RUN unzip -u -o /tmp/{{system_image_zip}} -d /sysimg/ 21 | 22 | FROM nvidia/opengl:1.2-glvnd-runtime-ubuntu20.04 23 | ENV NVIDIA_DRIVER_CAPABILITIES ${NVIDIA_DRIVER_CAPABILITIES},display 24 | 25 | # Now we configure the user account under which we will be running the emulator 26 | RUN mkdir -p /android/sdk/platforms && \ 27 | mkdir -p /android/sdk/platform-tools && \ 28 | mkdir -p /android/sdk/system-images 29 | 30 | # Make sure to place files that do not change often in the higher layers 31 | # as this will improve caching_ 32 | COPY platform-tools/adb /android/sdk/platform-tools/adb 33 | 34 | ENV ANDROID_SDK_ROOT /android/sdk 35 | WORKDIR /android/sdk 36 | COPY --from=sys_unzipper /sysimg/ /android/sdk/system-images/android/ 37 | RUN chmod +x /android/sdk/platform-tools/adb 38 | 39 | LABEL maintainer="{{user}}" \ 40 | ro.system.build.fingerprint="{{ro_system_build_fingerprint}}" \ 41 | ro.product.cpu.abi="{{ro_product_cpu_abi}}" \ 42 | ro.build.version.incremental="{{ro_build_version_incremental}}" \ 43 | ro.build.version.sdk="{{ro_build_version_sdk}}" \ 44 | ro.build.flavor="{{ro_build_flavor}}" \ 45 | ro.product.cpu.abilist="{{ro_product_cpu_abilist}}" \ 46 | ro.build.type="{{ro_build_type}}" \ 47 | SystemImage.TagId="{{SystemImage_TagId}}" \ 48 | qemu.tag="{{qemu_tag}}" \ 49 | qemu.cpu="{{qemu_cpu}}" \ 50 | qemu.short_tag="{{qemu_short_tag}}" \ 51 | qemu.short_abi="{{qemu_short_abi}}" 52 | 53 | # We adopt the following naimg convention -- 54 | # SystemImage.TagId in 'aosp', 'google', 'playstore'ß 55 | -------------------------------------------------------------------------------- /js/src/components/logcat_view.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import PropTypes from "prop-types"; 17 | import React, { Component } from "react"; 18 | import List from "@mui/material/List"; 19 | import ListItem from "@mui/material/ListItem"; 20 | import ListItemText from "@mui/material/ListItemText"; 21 | import { Logcat } from "android-emulator-webrtc/emulator"; 22 | /** 23 | * A very simple logcat viewer. It displays all logcat items in a material list. 24 | */ 25 | export default class LogcatView extends Component { 26 | state = { lines: [] }; 27 | 28 | static propTypes = { 29 | uri: PropTypes.string, // grpc endpoint 30 | auth: PropTypes.object, // auth module to use. 31 | maxHistory: PropTypes.number 32 | }; 33 | 34 | static defaultProps = { 35 | maxHistory: 2048 // Max nr of bytes. 36 | }; 37 | constructor(props) { 38 | super(props); 39 | const { uri, auth } = this.props; 40 | this.buffer = "" 41 | this.logcat = new Logcat(uri, auth); 42 | } 43 | 44 | componentDidMount = () => { 45 | this.logcat.start(this.onLogcat, 1000); 46 | }; 47 | 48 | componentWillUnmount = () => { 49 | this.logcat.stop(); 50 | }; 51 | 52 | onLogcat = logline => { 53 | const { maxHistory } = this.props 54 | this.buffer += logline; 55 | const sliceAt = this.buffer.length - maxHistory 56 | if (sliceAt > 0) { 57 | this.buffer = this.buffer.substr(this.buffer.indexOf('\n', sliceAt)); 58 | } 59 | this.setState({ lines: this.buffer.split('\n') }); 60 | }; 61 | 62 | asItems = loglines => { 63 | var i = 0; 64 | return loglines.map(line => ( 65 | 66 | 67 | 68 | )); 69 | }; 70 | 71 | render() { 72 | const { lines } = this.state; 73 | return {this.asItems(lines)}; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /emu/containers/progress_tracker.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from tqdm import tqdm 15 | 16 | 17 | class ProgressTracker: 18 | """ 19 | A class that tracks progress using tqdm for a set of layers that are pushed. 20 | """ 21 | 22 | def __init__(self): 23 | """ Initializes a new instance of ProgressTracker with an empty progress dictionary. """ 24 | self.progress = {} 25 | 26 | def __del__(self): 27 | """ Closes the tqdm progress bars for all entries in the progress dictionary. """ 28 | for _key, value in self.progress.items(): 29 | value["tqdm"].close() 30 | 31 | def update(self, entry): 32 | """Updates the progress bars given an entry dictionary.""" 33 | if "id" not in entry: 34 | return 35 | 36 | identity = entry["id"] 37 | if identity not in self.progress: 38 | self.progress[identity] = { 39 | "tqdm": tqdm(total=0, unit="B", unit_scale=True), # The progress bar 40 | "total": 0, # Total of bytes we are shipping 41 | "status": "", # Status message. 42 | "current": 0, # Current of total already send. 43 | } 44 | 45 | prog = self.progress[identity] 46 | total = int(entry.get("progressDetail", {}).get("total", -1)) 47 | current = int(entry.get("progressDetail", {}).get("current", 0)) 48 | 49 | if prog["total"] != total and total != -1: 50 | prog["total"] = total 51 | prog["tqdm"].reset(total=total) 52 | 53 | if prog["status"] != entry["status"]: 54 | prog["status"] = entry["status"] 55 | prog["tqdm"].set_description(f"{entry.get('status')} {identity}") 56 | 57 | if current != 0: 58 | diff = current - prog["current"] 59 | prog["current"] = current 60 | prog["tqdm"].update(diff) 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | bld/ 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | .docker-venv/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv/ 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | 125 | # Pyre type checker 126 | .pyre/ 127 | 128 | # Visual Studio Code. 129 | .vscode/* 130 | 131 | # Files used by our scripts 132 | *.zip 133 | /src 134 | 135 | # vscode's git plugin is not recursive. 136 | **/node_modules/ 137 | **/config.js 138 | -------------------------------------------------------------------------------- /js/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /js/src/service/auth_service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License") 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { EventEmitter } from "events"; 17 | 18 | /** 19 | * A TokenHolder that holds a JWT token. 20 | * 21 | * @export 22 | * @class TokenAuthService 23 | */ 24 | export class TokenProviderService { 25 | /** 26 | *Creates an instance of TokenAuthService. 27 | * @param {string} uri The endpoint that hands out a JWT Token. 28 | * @memberof TokenAuthService 29 | */ 30 | constructor() { 31 | this.events = new EventEmitter(); 32 | this.token = null; 33 | } 34 | 35 | setToken = (token) => { 36 | this.token = token; 37 | this.events.emit("authorized", true); 38 | } 39 | 40 | /** 41 | * Get notifications for state changes. 42 | * Currently only `authorized` is supported. 43 | * 44 | * @param {string} name of the event 45 | * @param {callback} fn To be invoked 46 | */ 47 | on = (name, fn) => { 48 | this.events.on(name, fn); 49 | }; 50 | 51 | 52 | /** 53 | * Logs the user out by resetting the token. 54 | * 55 | * @returns A promis 56 | */ 57 | logout = () => { 58 | return new Promise((resolve, reject) => { 59 | this.token = null; 60 | resolve(null); 61 | this.events.emit("authorized", false); 62 | }); 63 | }; 64 | 65 | /** 66 | * Called by the EmulatorWebClient when the web endpoint 67 | * returns a 401. It will fire the `authorized` event. 68 | * 69 | * @memberof TokenAuthService 70 | * @see EmulatorWebClient 71 | */ 72 | unauthorized = ev => { 73 | this.token = null; 74 | this.events.emit("authorized", false); 75 | }; 76 | 77 | /** 78 | * 79 | * @memberof TokenAuthService 80 | * @returns True if a token is set. 81 | */ 82 | authorized = () => { 83 | return this.token != null; 84 | }; 85 | 86 | /** 87 | * Called by the EmulatorWebClient to obtain the authentication 88 | * headers 89 | * 90 | * @memberof TokenAuthService 91 | * @returns The authentication header for the web call 92 | * @see EmulatorWebClient 93 | */ 94 | authHeader = () => { 95 | return { Authorization: this.token }; 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /emu/templates/Dockerfile.emulator: -------------------------------------------------------------------------------- 1 | # Copyright 2019 - The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | FROM {{from_base_img}} AS emulator 15 | 16 | # Install all the required emulator dependencies. 17 | # You can get these by running ./android/scripts/unix/run_tests.sh --verbose --verbose --debs | grep apt | sort -u 18 | # pulse audio is needed due to some webrtc dependencies. 19 | RUN apt-get update && apt-get install -y --no-install-recommends \ 20 | # Emulator & video bridge dependencies 21 | libc6 libdbus-1-3 libfontconfig1 libgcc1 \ 22 | libpulse0 libtinfo5 libx11-6 libxcb1 libxdamage1 \ 23 | libnss3 libxcomposite1 libxcursor1 libxi6 \ 24 | libxext6 libxfixes3 zlib1g libgl1 pulseaudio socat \ 25 | iputils-ping \ 26 | # Enable turncfg through usage of curl 27 | curl ca-certificates && \ 28 | apt-get clean && \ 29 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 30 | 31 | # Now we configure the user account under which we will be running the emulator 32 | RUN mkdir -p /android-home 33 | 34 | # Make sure to place files that do not change often in the higher layers 35 | # as this will improve caching. 36 | COPY launch-emulator.sh /android/sdk/ 37 | COPY default.pa /etc/pulse/default.pa 38 | 39 | RUN gpasswd -a root audio && \ 40 | chmod +x /android/sdk/launch-emulator.sh 41 | 42 | COPY emu/ /android/sdk/ 43 | COPY avd/ /android-home 44 | # Create an initial snapshot so we will boot fast next time around, 45 | # This is currently an experimental feature, and is not easily configurable// 46 | # RUN --security=insecure cd /android/sdk && ./launch-emulator.sh -quit-after-boot 120 47 | 48 | # This is the console port, you usually want to keep this closed. 49 | EXPOSE 5554 50 | 51 | # This is the ADB port, useful. 52 | EXPOSE 5555 53 | 54 | # This is the gRPC port, also useful, we don't want ADB to incorrectly identify this. 55 | EXPOSE 8554 56 | 57 | WORKDIR /android/sdk 58 | 59 | # You will need to make use of the grpc snapshot/webrtc functionality to actually interact with 60 | # the emulator. 61 | CMD ["/android/sdk/launch-emulator.sh"] 62 | 63 | # Note we should use gRPC status endpoint to check for health once the canary release is out. 64 | HEALTHCHECK --interval=30s \ 65 | --timeout=30s \ 66 | --start-period=30s \ 67 | --retries=3 \ 68 | CMD /android/sdk/platform-tools/adb shell getprop dev.bootcomplete | grep "1" 69 | 70 | LABEL maintainer="{{user}}" \ 71 | com.google.android.emulator.version="{{emu_build_id}}" 72 | -------------------------------------------------------------------------------- /emu/templates/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2019 - The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | {{from_base_img}} 15 | 16 | # Install all the required emulator dependencies. 17 | # You can get these by running ./android/scripts/unix/run_tests.sh --verbose --verbose --debs | grep apt | sort -u 18 | # pulse audio is needed due to some webrtc dependencies. 19 | RUN apt-get update && apt-get install -y --no-install-recommends \ 20 | # Emulator & video bridge dependencies 21 | libc6 libdbus-1-3 libfontconfig1 libgcc1 \ 22 | libpulse0 libtinfo5 libx11-6 libxcb1 libxdamage1 \ 23 | libnss3 libxcomposite1 libxcursor1 libxi6 \ 24 | libxext6 libxfixes3 zlib1g libgl1 pulseaudio socat \ 25 | # Enable turncfg through usage of curl 26 | curl ca-certificates && \ 27 | apt-get clean && \ 28 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 29 | 30 | # Now we configure the user account under which we will be running the emulator 31 | RUN mkdir -p /android-home 32 | 33 | # Make sure to place files that do not change often in the higher layers 34 | # as this will improve caching. 35 | COPY launch-emulator.sh /android/sdk/ 36 | COPY default.pa /etc/pulse/default.pa 37 | 38 | RUN gpasswd -a root audio && \ 39 | chmod +x /android/sdk/launch-emulator.sh 40 | 41 | COPY emu/ /android/sdk/ 42 | COPY avd/ /android-home 43 | # Create an initial snapshot so we will boot fast next time around, 44 | # This is currently an experimental feature, and is not easily configurable// 45 | # RUN --security=insecure cd /android/sdk && ./launch-emulator.sh -quit-after-boot 120 46 | 47 | # This is the console port, you usually want to keep this closed. 48 | EXPOSE 5554 49 | 50 | # This is the ADB port, useful. 51 | EXPOSE 5555 52 | 53 | # This is the gRPC port, also useful, we don't want ADB to incorrectly identify this. 54 | EXPOSE 8554 55 | 56 | WORKDIR /android/sdk 57 | 58 | # You will need to make use of the grpc snapshot/webrtc functionality to actually interact with 59 | # the emulator. 60 | CMD ["/android/sdk/launch-emulator.sh"] 61 | 62 | # Note we should use gRPC status endpoint to check for health once the canary release is out. 63 | HEALTHCHECK --interval=30s \ 64 | --timeout=30s \ 65 | --start-period=30s \ 66 | --retries=3 \ 67 | CMD /android/sdk/platform-tools/adb shell getprop dev.bootcomplete | grep "1" 68 | 69 | LABEL maintainer="{{user}}" \ 70 | com.google.android.emulator.description="Pixel 2 Emulator, running API {{api}}" \ 71 | com.google.android.emulator.version="{{tag}}-{{api}}-{{abi}}/{{emu_build_id}}" 72 | -------------------------------------------------------------------------------- /js/docker/certs/self_sign.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC4Wc+6VSUS8rJ6 3 | mqP4zqmyXDAvxLHdwzw56yFrCfErmDN/WKjPp2MKzFKA4OEw2UOWUNl07hudDV6O 4 | SiiJS1A1JmJZuwvxoX1m7INNHM53cqzFno2s0e6IXfcp3tFpIW2AjL0qsppcCe8y 5 | jA0Ei3mwS1hM8QuiVH7+VBemuU2iphBJ9jCLok//V/V1eXICf24GxTAovC+wJdjR 6 | kx/0NYhUzMLAx9TrYtwgXsRPI3mEIO1WbOGIVruvZJ+RomVnB6v+1nIJtYLOBBvg 7 | Up35uiLZtY/629uJxNJOStVKcpXay0cLpFZHCe1ZUrzeH4O/liycRKSFiJ+VImmh 8 | E9lBJDpWNVR1OjQXoY0T/lsDcaDbOlwElq1d+5UxGaJTmcnnoT89TmbxxXGeQ1gl 9 | u9ZtH3PK7XHecEAvpE+GnyfQ742iNUKNvr8wF4/uraP6eJMSQXFc6cR79CtsgM55 10 | DlN1xz/4laPT8M+42KLaTDIv+mFljYXp+ChXqqm4ZlgCAvH/PmxQ5RzUID5rOnwG 11 | 1/cZynPpjA9ylrJka51FQjvjr5WEDzR3YWeDzDJ6w2g6zFob5afClA0k2RJM8QDg 12 | A77cp6pPOT0p0eYEJPuiXRtAsHZt/Mah4MH1BT3tc8nhwowmw8PIlP2dQywiy6+P 13 | tsXHalgtfBu2dvPTPoZ5I7tBYHvn8wIDAQABAoICADIROkiJ7Vq7DVwc+aGORypI 14 | vVGL4x6ucoHsaRQQDC7h1EKmypozBMQe/90+tgo1R5Tgel62eEtsIR0V6PJ4wNze 15 | guGJ2lGSoWM9ot9jjnOEcoXtbN7d2SGyG3mEqW0bBgler9WT0jZjAFLDFJoCY1dM 16 | 7zteT+GTfzYFkrLWKs6cuVnNAhw6Re28bs6r4Bnrj+9IyK6XhYAal0s74PbLPy6A 17 | uffvjdUr8UrdUgWIRe1rn+nUAmCr5adZ9bhw2Ydk3wKELU6TvGXFWejPp1X2hpaI 18 | KAVihrpg+RkIW+svOaHFiZMQ29nJSWvz+5V8C6UR3SXHwsL9exHe2b9Ei8GUXD8t 19 | HBa3BrIVzp0K3ZPbUtEt/x6zO1cJnbALQf1gB/ukVy4XooLzJrmISCGMNAMP2eMJ 20 | b7ZEOFp6hxOoz8uNSkPkH+uRFpZDa6IQdtKFMo9uIuNXNNb1hl4gn+4xSTK4iPt8 21 | g9jBoUzgTsn5aFrTxNYr1QCGI0+TRWD+FXi6YIQlGnhp2uc0XHcmf5DnVn8Trlmt 22 | muP/bfZo0XrrvhuuWwRoXflaabTpJblIRO9/wx/Y3Lj1u2FUBrZmuQ+Q+UcNmdNa 23 | GH7yl5bq1wcXmmHjn3WOUZ/2Ms81maa0vbtxuCabU4mNXnfFeFBLasxUBnhWZiBL 24 | VD6ohQuWmRR+lNJuB17xAoIBAQDtre2QEIormqIxenK4HZLj7+7E7tMTzibrjeNC 25 | pmqLV7kwgW/oF7Obt32zBWxbzvJI4FXdGwf55pVY2xPAZCxGc5diJhQ56wdBTouh 26 | 7hxDnBOi94gmCADVZB9I4EcSK932CVNegX/9qvUxvTTkQG/XJi4e7TVFDZ5clpEH 27 | d5yE9bcgK6JlWNVU/tsPsDt4LAsHI39hpUsYu72RVfyRT7XzxowTJFkTrxw5WyIz 28 | 3HTcnHjZnc91XoM1T7dJkOruJYKrvt0eDdsyFByAVmytvRv8qRZplGJJitN1umXf 29 | qbNaSGAgFfxxv/px96WxrRcck0bObW7QGyCuc6Pc7FoijpX7AoIBAQDGj5AmGT0N 30 | sLPpSexuVqU5fniJupkLWaYa5IVAsj4vTpdzMUDUkWj+VQUAEEibwtYI73yF4BBs 31 | unYwHWoM3I2WRTyfKNw1EIJlk07hKDWngLkD8lS50zoXks/sUjPYiVaQXghsw/s+ 32 | tCXSwPnvHOtKJALaxbeROBvFkwfYlPF3E+JEuqOqw5dvmNLMEXHUREEDdCHjqMfv 33 | OVObigTprdM8fXE3+4V20VFsFq2p5kOxfNQgsSJWxYQSI/zJhzfUDotI2KblXwGM 34 | T/yC/AIMzR92XlIuZ3j7ZzabzhjPkwJ//Tb+1fp7RKMRkqMzjk5m2JwnieuXM8Se 35 | TY6Po2jRp+xpAoIBAEVBIK+RojECZbA1FahANcTk7JXFYQusTfrk5QtOokznyrYv 36 | gQQHo/YDiUTYl7JIoqBJfbtnXPOcFHhHnYG5roz4sWuP3OTjbsHAuT79uo6Ys8AQ 37 | kENEobmL6vG5J/xLe+ls3jXzVe/8GGXd08OOYwg1v8qTI6pzRWmFFc0vNRQo5Ksj 38 | C7asI70YZYw0tZ9WdgAmf79cAn26dooam/VbXJEjkT0iojHyHC86NsUVv5dooG4I 39 | ZaK1X1XxXF67MzkhBOo2Owe+0dfNtGBQzmnxaG4+dqFc8yaqpOLw5S4+rFvqKtgu 40 | j9g1MCx1FHqpDMruvvr3OAq7XNJ4L47372uSUHECggEBAJx00uiySmFXMuxHy3zF 41 | 2TsMZH5iAeXnpfLazgTEbitoif3CeYsFaO2+oEoEirHxPCWeT0hN3aNO6YHQK5gm 42 | 0Ynu1G21DI7ji1vuTuErhduOmjp54DjsL4ITtLJJs4CT9xmafpj1dCtcV9FRLZ8z 43 | 8lJhPb6UvKg4xelQiYYnFnz1tfzh88TCibjtemxB8qeHgJLwFyQEAkaFrVOJ1YUr 44 | 6p5nWab7EZcmKDo7RGvzfLOF4MBB0wT8bay9nppNabg2HZow3JEv41BkVlv/pr3f 45 | g0MJ30ehULsIAQeTxgkJlZa0N3llshEfbD6UhPRC1ZREagbdrj1eFTeHdSXJZPaO 46 | ksECggEAIb0QpiG8jURJGbvDQ4G7xFpK0GQn1OaHSCtgdUfLfKgMSFRSJ+g3RXUC 47 | 9AHtxSwr+NSe66Jq8bIyOLlL4fwiYl38vF6+6zP7n60/qZCmGsDJVUEcKCVbDj9I 48 | YmBA6O5nBw34E4xh9yaPukq9auYwOfPGOQ+YBsS5u0TQErbf1sK9235LnvqXZGV3 49 | 1lplPpPYOOxHchcojldDJY75LPys90E+/ZWFnGyYErThmtiC3DtcJXvvyXq97wyr 50 | gkpl/mKrU3folv7BsyH8dUxBOOlx43Y2rpA9iiGVYWwYmezOZlIltBTFYg/FrgEW 51 | dW8XOAfcrSaqHjXktCwdROcCN2nz/A== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /emu/containers/system_image_container.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import logging 15 | import os 16 | 17 | from emu.android_release_zip import SystemImageReleaseZip 18 | from emu.platform_tools import PlatformTools 19 | from emu.template_writer import TemplateWriter 20 | from emu.containers.docker_container import DockerContainer 21 | from emu.emu_downloads_menu import SysImgInfo 22 | 23 | 24 | class SystemImageContainer(DockerContainer): 25 | def __init__(self, sort, repo="us-docker.pkg.dev/android-emulator-268719/images"): 26 | super().__init__(repo) 27 | self.system_image_zip = None 28 | self.system_image_info = None 29 | 30 | if isinstance(sort, SysImgInfo): 31 | self.system_image_info = sort 32 | else: 33 | self.system_image_zip = SystemImageReleaseZip(sort) 34 | assert "ro.build.version.incremental" in self.system_image_zip.props 35 | 36 | def _copy_adb_to(self, dest): 37 | """Find adb, or download it if needed.""" 38 | logging.info("Retrieving platform-tools") 39 | tools = PlatformTools() 40 | tools.extract_adb(dest) 41 | 42 | def write(self, destination): 43 | # We do not really want to overwrite if the files already exist. 44 | # Make sure the destination directory is empty. 45 | if self.system_image_zip is None: 46 | logging.info("Downloading zip file to %s", destination) 47 | self.system_image_zip = SystemImageReleaseZip( 48 | self.system_image_info.download(destination) 49 | ) 50 | 51 | writer = TemplateWriter(destination) 52 | self._copy_adb_to(destination) 53 | 54 | props = self.system_image_zip.props 55 | dest_zip = os.path.basename(self.system_image_zip.copy(destination)) 56 | props["system_image_zip"] = dest_zip 57 | writer.write_template( 58 | "Dockerfile.system_image", 59 | props, 60 | rename_as="Dockerfile", 61 | ) 62 | 63 | def image_name(self): 64 | if self.system_image_info: 65 | return self.system_image_info.image_name() 66 | if self.system_image_zip: 67 | return f"sys-{self.system_image_zip.api()}-{self.system_image_zip.short_tag()}-{self.system_image_zip.short_abi()}" 68 | 69 | def docker_tag(self): 70 | if self.system_image_zip: 71 | return self.system_image_zip.props["ro.build.version.incremental"] 72 | 73 | if super().available(): 74 | return self.image_labels()["ro.build.version.incremental"] 75 | 76 | # Unknown, revert to latest. 77 | return "latest" 78 | 79 | def image_labels(self): 80 | if self.docker_image(): 81 | return self.docker_image().labels 82 | return self.system_image_zip.props 83 | 84 | def depends_on(self): 85 | return "-" 86 | -------------------------------------------------------------------------------- /js/src/components/login_firebase.js: -------------------------------------------------------------------------------- 1 | import withStyles from '@mui/styles/withStyles'; 2 | 3 | import Avatar from "@mui/material/Avatar"; 4 | import Box from "@mui/material/Box"; 5 | import Button from "@mui/material/Button"; 6 | import Container from "@mui/material/Container"; 7 | import Copyright from "./copyright"; 8 | import CssBaseline from "@mui/material/CssBaseline"; 9 | import LockOutlinedIcon from "@mui/icons-material/LockOutlined"; 10 | import PropTypes from "prop-types"; 11 | import React from "react"; 12 | import Typography from "@mui/material/Typography"; 13 | import * as firebase from "firebase/app"; 14 | import "firebase/auth"; 15 | import { 16 | FirebaseAuthProvider, 17 | FirebaseAuthConsumer 18 | } from "@react-firebase/auth"; 19 | import { config } from "../config"; 20 | 21 | const useStyles = theme => ({ 22 | "@global": { 23 | body: { 24 | backgroundColor: theme.palette.common.white 25 | } 26 | }, 27 | paper: { 28 | marginTop: theme.spacing(8), 29 | display: "flex", 30 | flexDirection: "column", 31 | alignItems: "center" 32 | }, 33 | avatar: { 34 | margin: theme.spacing(1), 35 | backgroundColor: theme.palette.secondary.main 36 | }, 37 | form: { 38 | width: "100%", // Fix IE 11 issue. 39 | marginTop: theme.spacing(1) 40 | }, 41 | submit: { 42 | margin: theme.spacing(3, 0, 2) 43 | } 44 | }); 45 | 46 | // A basic login page using firebase. 47 | class SignIn extends React.Component { 48 | state = { 49 | email: "", 50 | password: "", 51 | }; 52 | 53 | static propTypes = { 54 | auth: PropTypes.object.isRequired // Auth service 55 | }; 56 | 57 | loginGoogle = () => { 58 | const googleAuthProvider = new firebase.auth.GoogleAuthProvider(); 59 | firebase.auth().signInWithPopup(googleAuthProvider); 60 | } 61 | 62 | render() { 63 | const { classes, auth } = this.props; 64 | 65 | return ( 66 | 67 | 68 | 69 |
70 | 71 | 72 | 73 | Sign in 74 | 75 | 76 | 77 |
78 | 79 | 80 | 81 |
82 | 83 | {({ isSignedIn, user, providerId }) => { 84 | console.log(`Firebase login: isSignedId: ${isSignedIn}`) 85 | if (isSignedIn) { 86 | firebase.auth().currentUser.getIdToken().then(function (token) { 87 | auth.setToken(`Bearer ${token}`); 88 | }); 89 | } 90 | }} 91 | 92 |
93 | 94 | ); 95 | } 96 | } 97 | 98 | export default withStyles(useStyles)(SignIn); 99 | -------------------------------------------------------------------------------- /emu/template_writer.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import logging 15 | from pathlib import Path 16 | from typing import Dict 17 | 18 | from jinja2 import Environment, PackageLoader 19 | 20 | 21 | class TemplateWriter: 22 | """A Template writer uses Jinja to fill in templates. 23 | 24 | All the templates should live in the ./emu/templates directory. 25 | """ 26 | 27 | def __init__(self, out_dir: str) -> None: 28 | """Creates a template writer that writes templates to the out_dir. 29 | 30 | The out directory will be created if needed. 31 | 32 | Args: 33 | out_dir (str): The directory where templates will be written. 34 | """ 35 | self.env = Environment(loader=PackageLoader("emu", "templates")) 36 | self.dest = Path(out_dir) 37 | 38 | def _jinja_safe_dict(self, props: Dict[str, str]) -> Dict[str, str]: 39 | """Replaces all the . with _ in the keys of a dictionary. 40 | 41 | Args: 42 | props (dict): The dictionary to normalize. 43 | 44 | Returns: 45 | dict: A dictionary with keys normalized by replacing . with _. 46 | """ 47 | return {key.replace(".", "_"): val for key, val in props.items()} 48 | 49 | def write_template( 50 | self, template_file: str, template_dict: Dict[str, str], rename_as: str = None 51 | ) -> Path: 52 | """Fill out the given template, writing it to the destination directory. 53 | 54 | Args: 55 | template_file (str): The name of the template file to fill. 56 | template_dict (dict): The dictionary to use to fill in the template. 57 | rename_as (str, optional): The name to use for the output file. Defaults to None. 58 | 59 | Returns: 60 | Path: The path to the written file. 61 | """ 62 | dest_name = rename_as if rename_as else template_file 63 | return self._write_template_to( 64 | template_file, self.dest / dest_name, template_dict 65 | ) 66 | 67 | def _write_template_to( 68 | self, tmpl_file: str, dest_file: Path, template_dict: Dict[str, str] 69 | ) -> None: 70 | """Loads the given template, writing it to the dest_file. 71 | 72 | Note: the template will be written {dest_dir}/{tmpl_file}, 73 | directories will be created if the do not yet exist. 74 | 75 | Args: 76 | tmpl_file (str): The name of the template file. 77 | dest_file (pathlib.Path): The path to the file to be written. 78 | template_dict (dict): The dictionary to use to fill in the template. 79 | """ 80 | template = self.env.get_template(tmpl_file) 81 | safe_dict = self._jinja_safe_dict(template_dict) 82 | dest_file.parent.mkdir(parents=True, exist_ok=True) 83 | logging.info("Writing: %s -> %s with %s", tmpl_file, dest_file, safe_dict) 84 | with open(dest_file, "wb") as dfile: 85 | dfile.write(template.render(safe_dict).encode("utf-8")) 86 | -------------------------------------------------------------------------------- /emu/templates/emulator.README.MD: -------------------------------------------------------------------------------- 1 | ## Android Emulator {{emu_build_id}} running Android-{{ro_build_version_sdk}} with {{qemu_tag}} 2 | The Android Emulator simulates Android devices on your computer so that you can test your application on a variety of devices and Android API levels without needing to have each physical device. 3 | 4 | The emulator provides almost all of the capabilities of a real Android device. You can simulate incoming phone calls and text messages, specify the location of the device, simulate different network speeds, simulate rotation and other hardware sensors, access the Google Play Store, and much more. 5 | 6 | Testing your app on the emulator is in some ways faster and easier than doing so on a physical device. 7 | 8 | ### Requirements and recommendations 9 | The docker images have the following requirements: 10 | 11 | - Linux only. Launching the emulator in Windows or MacOS is not supported. 12 | - KVM must be available. You can get access to KVM by running on "bare metal", 13 | or on a (virtual) machine that provides [nested 14 | virtualization](https://blog.turbonomic.com/blog/). If you are planning to run 15 | this in the cloud (gce/azure/aws/etc..) you first must make sure you have 16 | access to KVM. Details on how to get access to KVM on the various cloud 17 | providers can be found here: 18 | 19 | - AWS provides [bare 20 | metal](https://aws.amazon.com/about-aws/whats-new/2019/02/introducing-five-new-amazon-ec2-bare-metal-instances/) 21 | instances that provide access to KVM. 22 | - Azure: Follow these 23 | [instructions](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/nested-virtualization) 24 | to enable nested virtualization. 25 | - GCE: Follow these 26 | [instructions](https://cloud.google.com/compute/docs/instances/enable-nested-virtualization-vm-instances) 27 | to enable nested virtualization. 28 | 29 | Keep in mind that you will see reduced performance if you are making use of 30 | nested virtualization. 31 | 32 | ### Usage 33 | 34 | The following environment variables are accepted by the emulator: 35 | 36 | - `TOKEN` This is the console authorization token. The console is available on port 5554 37 | - `ADBKEY` This is the private adb key, needed if you wish to make use of adb. ADB is available on port 5555. 38 | 39 | The following ports are available: 40 | 41 | - 5554: The emulator [console](https://developer.android.com/studio/run/emulator-console) port. 42 | - 5555: The [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) port 43 | - 8555: The gRPC port. This can be used by android studio and javascript endpoints. 44 | 45 | An example invocation making the console, adb and the gRPC endpoint available: 46 | ```sh 47 | docker run -e "TOKEN=$(cat ~/.emulator_console_auth_token)" -e "ADBKEY=$(cat ~/.android/adbkey)" -e "EMULATOR_PARAMS=${PARAMS}" --device /dev/kvm --publish 8554:8554/tcp --publish 5554:5554/tcp --publish 5555:5555/tcp ${CONTAINER_ID} 48 | ``` 49 | 50 | You might need to run `adb connect localhost:5555` to enable ADB to discover the device 51 | 52 | ### License 53 | 54 | By making use of this container you accept the [Android Sdk License](https://developer.android.com/studio/terms) 55 | 56 | The android emulator is released under the [Apache-2 license](http://www.apache.org/licenses/LICENSE-2.0). 57 | 58 | The notices for the emulator and system image can be found in the following directories: 59 | 60 | - /android/sdk/system-images/android/x86_64/NOTICE.txt 61 | - /android/sdk/system-images/android/x86/NOTICE.txt 62 | - /android/sdk/emulator/NOTICE.txt 63 | 64 | You can extract the notices as follows: 65 | 66 | CONTAINER_ID=$(docker create {{container_id}}) 67 | docker export $CONTAINER_ID | tar vxf - --wildcards --no-anchored 'NOTICE.txt' 68 | 69 | 70 | ### Metrics 71 | 72 | {{metrics}} -------------------------------------------------------------------------------- /emu/templates/registry.README.MD: -------------------------------------------------------------------------------- 1 | ## Android Emulator 2 | The Android Emulator simulates Android devices on your computer so that you can test your application on a variety of devices and Android API levels without needing to have each physical device. 3 | 4 | The emulator provides almost all of the capabilities of a real Android device. You can simulate incoming phone calls and text messages, specify the location of the device, simulate different network speeds, simulate rotation and other hardware sensors, access the Google Play Store, and much more. 5 | 6 | Testing your app on the emulator is in some ways faster and easier than doing so on a physical device. 7 | 8 | ### Requirements and recommendations 9 | The docker images have the following requirements: 10 | 11 | - Linux only. Launching the emulator in Windows or MacOS is not supported. 12 | - KVM must be available. You can get access to KVM by running on "bare metal", 13 | or on a (virtual) machine that provides [nested 14 | virtualization](https://blog.turbonomic.com/blog/). If you are planning to run 15 | this in the cloud (gce/azure/aws/etc..) you first must make sure you have 16 | access to KVM. Details on how to get access to KVM on the various cloud 17 | providers can be found here: 18 | 19 | - AWS provides [bare 20 | metal](https://aws.amazon.com/about-aws/whats-new/2019/02/introducing-five-new-amazon-ec2-bare-metal-instances/) 21 | instances that provide access to KVM. 22 | - Azure: Follow these 23 | [instructions](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/nested-virtualization) 24 | to enable nested virtualization. 25 | - GCE: Follow these 26 | [instructions](https://cloud.google.com/compute/docs/instances/enable-nested-virtualization-vm-instances) 27 | to enable nested virtualization. 28 | 29 | Keep in mind that you will see reduced performance if you are making use of nested virtualization. 30 | 31 | ### Available images. 32 | 33 | Here is a list of the latest images. 34 | 35 | {{emu_images}} 36 | 37 | For example you can now pull a container as follows: 38 | 39 | docker pull {{first_image}} 40 | 41 | ### Usage 42 | 43 | The following environment variables are accepted by the emulator: 44 | 45 | - `ADBKEY` This is the private adb key, needed if you wish to make use of adb. ADB is available on port 5555. 46 | 47 | The following ports are available: 48 | 49 | - 5555: The [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) port 50 | - 8555: The gRPC port. This can be used by android studio and javascript endpoints. 51 | 52 | An example invocation making the console, adb and the gRPC endpoint available: 53 | ```sh 54 | docker run -e "ADBKEY=$(cat ~/.android/adbkey)" --device /dev/kvm --publish 8554:8554/tcp --publish 5554:5554/tcp --publish 5555:5555/tcp {{first_image}} 55 | ``` 56 | 57 | You might need to run `adb connect localhost:5555` to enable ADB to discover the device. 58 | 59 | 60 | ### License 61 | 62 | By making use of this container you accept the [Android Sdk License](https://developer.android.com/studio/terms) 63 | 64 | The android emulator is released under the [Apache-2 license](http://www.apache.org/licenses/LICENSE-2.0). 65 | 66 | The notices for the emulator and system image can be found in the following directories: 67 | 68 | - /android/sdk/system-images/android/x86_64/NOTICE.txt 69 | - /android/sdk/system-images/android/x86/NOTICE.txt 70 | - /android/sdk/emulator/NOTICE.txt 71 | 72 | You can extract the notices as follows: 73 | 74 | CONTAINER_ID=$(docker create name_of_image) 75 | docker export $CONTAINER_ID | tar vxf - --wildcards --no-anchored 'NOTICE.txt' 76 | 77 | 78 | For containers that do not have the -no-metrics you accept the following: 79 | 80 | By using this docker container you authorize Google to collect usage data for the Android Emulator 81 | — such as how you utilize its features and resources, and how you use it to test applications. 82 | This data helps improve the Android Emulator and is collected in accordance with 83 | [Google's Privacy Policy](http://www.google.com/policies/privacy/) 84 | -------------------------------------------------------------------------------- /create_web_container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright 2019 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | DOCKER_YAML=js/docker/docker-compose-build.yaml 17 | 18 | # Fancy colors in the terminal 19 | if [ -t 1 ]; then 20 | RED=$(tput setaf 1) 21 | GREEN=$(tput setaf 2) 22 | RESET=$(tput sgr0) 23 | else 24 | RED= 25 | GREEN= 26 | RESET= 27 | fi 28 | 29 | approve() { 30 | echo "${GREEN}I know what I'm doing..[y/n]?${RESET}" 31 | old_stty_cfg=$(stty -g) 32 | stty raw -echo 33 | answer=$(head -c 1) 34 | stty $old_stty_cfg # Careful playing with stty 35 | if echo "$answer" | grep -iq "^y"; then 36 | echo Yes 37 | else 38 | echo Ok, bye! 39 | exit 1 40 | fi 41 | } 42 | 43 | help() { 44 | cat </dev/null 2>&1; then 69 | ADB=$ANDROID_SDK_ROOT/platform-tools/adb 70 | command -v $ADB >/dev/null 2>&1 || panic "No adb key, and adb not found in $ADB, make sure ANDROID_SDK_ROOT is set!" 71 | fi 72 | echo "Creating public key from private key with $ADB" 73 | $ADB keygen ~/.android/adbkey 74 | fi 75 | } 76 | 77 | while getopts 'hasi:' flag; do 78 | case "${flag}" in 79 | a) DOCKER_YAML="${DOCKER_YAML} -f js/docker/development.yaml" ;; 80 | h) help ;; 81 | s) START='yes' ;; 82 | i) INSTALL='yes' ;; 83 | *) help ;; 84 | esac 85 | done 86 | 87 | 88 | # Create the javascript protobufs 89 | make -C js deps 90 | 91 | # Make sure we have all we need for adb to succeed. 92 | generate_keys 93 | 94 | 95 | # Copy the private adbkey over 96 | cp ~/.android/adbkey js/docker/certs 97 | 98 | # compose the container 99 | python -m venv .docker-venv 100 | source .docker-venv/bin/activate 101 | pip install docker-compose 102 | docker-compose -f ${DOCKER_YAML} build 103 | rm js/docker/certs/adbkey 104 | 105 | if [ "${START}" = "yes" ]; then 106 | docker-compose -f ${DOCKER_YAML} up 107 | else 108 | echo "Created container, you can launch it as follows:" 109 | echo "docker-compose -f ${DOCKER_YAML} up" 110 | fi 111 | 112 | if [ "${INSTALL}" = "yes" ]; then 113 | echo "Installing created container as systemd service" 114 | echo "This will copy the docker yaml files in /opt/emulator" 115 | echo "Make the current adbkey available to the image" 116 | echo "And activate the container as a systemd service." 117 | approve 118 | 119 | sudo mkdir -p /opt/emulator 120 | sudo cp ~/.android/adbkey /opt/emulator/adbkey 121 | sudo cp js/docker/docker-compose.yaml /opt/emulator/docker-compose.yaml 122 | sudo cp js/docker/production.yaml /opt/emulator/docker-compose.override.yaml 123 | sudo cp js/docker/emulator.service /etc/systemd/system/emulator.service 124 | sudo touch /etc/ssl/certs/emulator-grpc.cer 125 | sudo touch /etc/ssl/private/emulator-grpc.key 126 | sudo systemctl daemon-reload 127 | sudo systemctl enable emulator.service 128 | sudo systemctl restart emulator.service 129 | fi 130 | -------------------------------------------------------------------------------- /tests/e2e/test_containers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """End 2 end test that builds every image. 15 | 16 | This is super expensive.. 17 | """ 18 | 19 | import collections 20 | import subprocess 21 | import sys 22 | 23 | import docker 24 | import pytest 25 | from utils import find_adb, find_free_port 26 | 27 | import emu.emu_docker as emu_docker 28 | 29 | linux_only = pytest.mark.skipif( 30 | not sys.platform.startswith("linux"), reason="launching containers requires kvm, only available in linux" 31 | ) 32 | 33 | Arguments = collections.namedtuple( 34 | "Args", "emuzip, imgzip, dest, tag, start, extra, gpu, accept, metrics, no_metrics, repo, push, sys" 35 | ) 36 | 37 | 38 | def test_has_docker(): 39 | assert docker.from_env().ping() 40 | 41 | 42 | @pytest.mark.slow 43 | @pytest.mark.e2e 44 | @pytest.mark.parametrize("channel, img, gpu", [("all", "Q google_apis x86_64", True)]) 45 | def test_build_container(temp_dir, channel, img, gpu): 46 | assert docker.from_env().ping() 47 | # Make sure we accept all licenses, 48 | args = Arguments( 49 | channel, 50 | img, 51 | temp_dir, 52 | None, 53 | False, 54 | "", 55 | gpu, 56 | True, 57 | False, 58 | False, 59 | "us-docker.pkg.dev/android-emulator-268719/images", 60 | False, 61 | False, 62 | ) 63 | emu_docker.accept_licenses(args) 64 | devices = emu_docker.create_docker_image(args) 65 | assert devices 66 | for device in devices: 67 | assert device.image_name() is not None 68 | client = docker.from_env() 69 | assert client.images.get(device.full_name()) is not None 70 | 71 | 72 | @pytest.mark.slow 73 | @pytest.mark.e2e 74 | @pytest.mark.parametrize("channel, img, gpu", [("stable", "P android x86_64", False)]) 75 | @linux_only 76 | def test_run_container(temp_dir, channel, img, gpu): 77 | args = Arguments( 78 | channel, 79 | img, 80 | temp_dir, 81 | None, 82 | False, 83 | "", 84 | gpu, 85 | True, 86 | False, 87 | False, 88 | "us-docker.pkg.dev/android-emulator-268719/images", 89 | False, 90 | False, 91 | ) 92 | emu_docker.accept_licenses(args) 93 | devices = emu_docker.create_docker_image(args) 94 | assert devices 95 | for device in devices: 96 | port = find_free_port() 97 | 98 | # Launch this thing. 99 | container = device.launch({"5555/tcp": port}) 100 | # Now we are going to insepct this thing. 101 | api_client = device.get_api_client() 102 | status = api_client.inspect_container(container.id) 103 | state = status["State"] 104 | assert state["Status"] == "running" 105 | 106 | # Acceptable states: 107 | # starting --> We are still launching 108 | # healthy --> Yay, we booted! Good to go.. 109 | health = state["Health"]["Status"] 110 | while health == "starting": 111 | health = api_client.inspect_container(container.id)["State"]["Health"]["Status"] 112 | 113 | assert health == "healthy" 114 | 115 | # Good, good.. From an internal perspective things look great. 116 | # Can we connect with adb from outside the container? 117 | adb = find_adb() 118 | 119 | # Erase knowledge of existing devices. 120 | subprocess.check_output([adb, "kill-server"]) 121 | name = "localhost:{}".format(port) 122 | subprocess.check_output([adb, "connect", name]) 123 | 124 | # Boot complete should be true.. 125 | res = subprocess.check_output([adb, "-s", name, "shell", "getprop", "dev.bootcomplete"]) 126 | assert "1" in str(res) 127 | 128 | api_client.stop(container.id) 129 | -------------------------------------------------------------------------------- /emu/docker_config.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Module that makes sure you have accepted the proper license. 15 | """ 16 | from configparser import ConfigParser 17 | from pathlib import Path 18 | from typing import Union 19 | 20 | from appdirs import user_config_dir 21 | 22 | 23 | class DockerConfig: 24 | """Class for managing Docker configuration.""" 25 | 26 | def __init__(self): 27 | """Initialize DockerConfig object.""" 28 | cfg_dir: Path = Path(user_config_dir("emu-docker", "Google")) 29 | if not cfg_dir.exists(): 30 | cfg_dir.mkdir(parents=True) 31 | 32 | self.cfg_file: Path = cfg_dir / "goole-emu-docker.config" 33 | self.cfg: ConfigParser = ConfigParser() 34 | self._load_config() 35 | 36 | def collect_metrics(self) -> bool: 37 | """Check if the user is okay with collecting metrics. 38 | 39 | Returns: 40 | bool: True if the user is okay with collecting metrics, False otherwise. 41 | """ 42 | return self._cfg_true("metrics") 43 | 44 | def set_collect_metrics(self, to_collect: bool): 45 | """Set whether to collect metrics. 46 | 47 | Args: 48 | to_collect (bool): True to collect metrics, False otherwise. 49 | """ 50 | self._set_cfg("metrics", str(to_collect)) 51 | 52 | def decided_on_metrics(self) -> bool: 53 | """Check if the user has made a choice around metrics collection. 54 | 55 | Returns: 56 | bool: True if the user has made a choice, False otherwise. 57 | """ 58 | return self._has_cfg("metrics") 59 | 60 | def accepted_license(self, agreement: str) -> bool: 61 | """Check if the user has accepted the given license agreement. 62 | 63 | Args: 64 | agreement (str): The license agreement to check. 65 | 66 | Returns: 67 | bool: True if the user has accepted the license agreement, False otherwise. 68 | """ 69 | return self._cfg_true(agreement) 70 | 71 | def accept_license(self, agreement: str): 72 | """Accept the given license agreement. 73 | 74 | Args: 75 | agreement (str): The license agreement to accept. 76 | """ 77 | self._set_cfg(agreement) 78 | 79 | def _cfg_true(self, label: str) -> bool: 80 | """Check if the specified label is true in the configuration. 81 | 82 | Args: 83 | label (str): The label to check. 84 | 85 | Returns: 86 | bool: True if the label is set to True in the configuration, False otherwise. 87 | """ 88 | if self._has_cfg(label): 89 | return "True" in self.cfg["DEFAULT"][label] 90 | return False 91 | 92 | def _set_cfg(self, label: str, state: str = "True"): 93 | """Set the specified label in the configuration. 94 | 95 | Args: 96 | label (str): The label to set. 97 | state (str, optional): The state to set (default is "True"). 98 | """ 99 | self._load_config() 100 | self.cfg["DEFAULT"][label] = state 101 | self._save_config() 102 | 103 | def _has_cfg(self, label: str) -> bool: 104 | """Check if the specified label exists in the configuration. 105 | 106 | Args: 107 | label (str): The label to check. 108 | 109 | Returns: 110 | bool: True if the label exists in the configuration, False otherwise. 111 | """ 112 | return label in self.cfg["DEFAULT"] 113 | 114 | def _save_config(self): 115 | """Save the configuration to the config file.""" 116 | with open(self.cfg_file, "w", encoding="utf-8") as cfgfile: 117 | self.cfg.write(cfgfile) 118 | 119 | def _load_config(self): 120 | """Load the configuration from the config file if it exists.""" 121 | if self.cfg_file.exists(): 122 | self.cfg.read(self.cfg_file) 123 | -------------------------------------------------------------------------------- /REGISTRY.MD: -------------------------------------------------------------------------------- 1 | ## Android Emulator 2 | The Android Emulator simulates Android devices on your computer so that you can test your application on a variety of devices and Android API levels without needing to have each physical device. 3 | 4 | The emulator provides almost all of the capabilities of a real Android device. You can simulate incoming phone calls and text messages, specify the location of the device, simulate different network speeds, simulate rotation and other hardware sensors, access the Google Play Store, and much more. 5 | 6 | Testing your app on the emulator is in some ways faster and easier than doing so on a physical device. 7 | 8 | ### Requirements and recommendations 9 | The docker images have the following requirements: 10 | 11 | - Linux only. Launching the emulator in Windows or MacOS is not supported. 12 | - KVM must be available. You can get access to KVM by running on "bare metal", 13 | or on a (virtual) machine that provides [nested 14 | virtualization](https://blog.turbonomic.com/blog/). If you are planning to run 15 | this in the cloud (gce/azure/aws/etc..) you first must make sure you have 16 | access to KVM. Details on how to get access to KVM on the various cloud 17 | providers can be found here: 18 | 19 | - AWS provides [bare 20 | metal](https://aws.amazon.com/about-aws/whats-new/2019/02/introducing-five-new-amazon-ec2-bare-metal-instances/) 21 | instances that provide access to KVM. 22 | - Azure: Follow these 23 | [instructions](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/nested-virtualization) 24 | to enable nested virtualization. 25 | - GCE: Follow these 26 | [instructions](https://cloud.google.com/compute/docs/instances/enable-nested-virtualization-vm-instances) 27 | to enable nested virtualization. 28 | 29 | Keep in mind that you will see reduced performance if you are making use of nested virtualization. 30 | 31 | ### Available images. 32 | 33 | Here is a list of the latest images. 34 | 35 | * us-docker.pkg.dev/android-emulator-268719/images/28-playstore-x64:30.1.2 36 | * us-docker.pkg.dev/android-emulator-268719/images/28-playstore-x64-no-metrics:30.1.2 37 | * us-docker.pkg.dev/android-emulator-268719/images/29-google-x64:30.1.2 38 | * us-docker.pkg.dev/android-emulator-268719/images/29-google-x64-no-metrics:30.1.2 39 | * us-docker.pkg.dev/android-emulator-268719/images/30-google-x64:30.1.2 40 | * us-docker.pkg.dev/android-emulator-268719/images/30-google-x64-no-metrics:30.1.2 41 | 42 | For example you can now pull a container as follows: 43 | 44 | docker pull us-docker.pkg.dev/android-emulator-268719/images/28-playstore-x64:30.1.2 45 | 46 | ### Usage 47 | 48 | The following environment variables are accepted by the emulator: 49 | 50 | - `ADBKEY` This is the private adb key, needed if you wish to make use of adb. ADB is available on port 5555. 51 | 52 | The following ports are available: 53 | 54 | - 5555: The [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) port 55 | - 8555: The gRPC port. This can be used by android studio and javascript endpoints. 56 | 57 | An example invocation making the console, adb and the gRPC endpoint available: 58 | ```sh 59 | docker run -e "ADBKEY=$(cat ~/.android/adbkey)" --device /dev/kvm --publish 8554:8554/tcp --publish 5554:5554/tcp --publish 5555:5555/tcp us-docker.pkg.dev/android-emulator-268719/images/28-playstore-x64:30.1.2 60 | ``` 61 | 62 | You might need to run `adb connect localhost:5555` to enable ADB to discover the device. 63 | 64 | 65 | ### License 66 | 67 | By making use of this container you accept the [Android Sdk License](https://developer.android.com/studio/terms) 68 | 69 | The android emulator is released under the [Apache-2 license](http://www.apache.org/licenses/LICENSE-2.0). 70 | 71 | The notices for the emulator and system image can be found in the following directories: 72 | 73 | - /android/sdk/system-images/android/x86_64/NOTICE.txt 74 | - /android/sdk/system-images/android/x86/NOTICE.txt 75 | - /android/sdk/emulator/NOTICE.txt 76 | 77 | You can extract the notices as follows: 78 | 79 | CONTAINER_ID=$(docker create name_of_image) 80 | docker export $CONTAINER_ID | tar vxf - --wildcards --no-anchored 'NOTICE.txt' 81 | 82 | 83 | For containers that do not have the -no-metrics you accept the following: 84 | 85 | By using this docker container you authorize Google to collect usage data for the Android Emulator 86 | — such as how you utilize its features and resources, and how you use it to test applications. 87 | This data helps improve the Android Emulator and is collected in accordance with 88 | [Google's Privacy Policy](http://www.google.com/policies/privacy/) -------------------------------------------------------------------------------- /emu/containers/emulator_container.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import os 15 | import shutil 16 | 17 | import emu 18 | from emu.android_release_zip import AndroidReleaseZip 19 | from emu.containers.docker_container import DockerContainer 20 | from emu.template_writer import TemplateWriter 21 | 22 | 23 | class EmulatorContainer(DockerContainer): 24 | 25 | METRICS_MESSAGE = """ 26 | By using this docker container you authorize Google to collect usage data for the Android Emulator 27 | — such as how you utilize its features and resources, and how you use it to test applications. 28 | This data helps improve the Android Emulator and is collected in accordance with 29 | [Google's Privacy Policy](http://www.google.com/policies/privacy/) 30 | """ 31 | NO_METRICS_MESSAGE = "No metrics are collected when running this container." 32 | 33 | def __init__( 34 | self, emulator, system_image_container, repository=None, metrics=False, extra="" 35 | ): 36 | self.emulator_zip = AndroidReleaseZip(emulator) 37 | self.system_image_container = system_image_container 38 | self.metrics = metrics 39 | 40 | if type(extra) is list: 41 | extra = " ".join([f'"{s}"' for s in extra]) 42 | 43 | cpu = system_image_container.image_labels()["ro.product.cpu.abi"] 44 | self.extra = self._logger_flags(cpu) + " " + extra 45 | 46 | metrics_msg = EmulatorContainer.NO_METRICS_MESSAGE 47 | if metrics: 48 | self.extra += ' "-metrics-collection"' 49 | metrics_msg = EmulatorContainer.METRICS_MESSAGE 50 | 51 | self.props = system_image_container.image_labels() 52 | self.props["playstore"] = self.props["qemu.tag"] == "google_apis_playstore" 53 | self.props["metrics"] = metrics_msg 54 | self.props["emu_build_id"] = self.emulator_zip.build_id() 55 | self.props["from_base_img"] = system_image_container.full_name() 56 | 57 | for expect in [ 58 | "ro.build.version.sdk", 59 | "qemu.tag", 60 | "qemu.short_tag", 61 | "qemu.short_abi", 62 | "ro.product.cpu.abi", 63 | ]: 64 | assert expect in self.props, "{} is not in {}".format(expect, self.props) 65 | 66 | super().__init__(repository) 67 | 68 | def _logger_flags(self, cpu): 69 | if "arm" in cpu: 70 | return '"-logcat" "*:V" "-show-kernel"' 71 | else: 72 | return '"-shell-serial" "file:/tmp/android-unknown/kernel.log" "-logcat-output" "/tmp/android-unknown/logcat.log"' 73 | 74 | def write(self, dest): 75 | # Make sure the destination directory is empty. 76 | self.clean(dest) 77 | 78 | writer = TemplateWriter(dest) 79 | writer.write_template("avd/Pixel2.ini", self.props) 80 | writer.write_template("avd/Pixel2.avd/config.ini", self.props) 81 | 82 | # Include a README.MD message. 83 | writer.write_template( 84 | "emulator.README.MD", 85 | self.props, 86 | rename_as="README.MD", 87 | ) 88 | 89 | writer.write_template( 90 | "launch-emulator.sh", {"extra": self.extra, "version": emu.__version__} 91 | ) 92 | writer.write_template("default.pa", {}) 93 | 94 | writer.write_template( 95 | "Dockerfile.emulator", 96 | self.props, 97 | rename_as="Dockerfile", 98 | ) 99 | 100 | self.emulator_zip.extract(os.path.join(dest, "emu")) 101 | 102 | def image_name(self): 103 | name = "{}-{}-{}".format( 104 | self.props["ro.build.version.sdk"], 105 | self.props["qemu.short_tag"], 106 | self.props["qemu.short_abi"], 107 | ) 108 | if not self.metrics: 109 | return "{}-no-metrics".format(name) 110 | return name 111 | 112 | def docker_tag(self): 113 | return self.props["emu_build_id"] 114 | 115 | def depends_on(self): 116 | if not self.system_image_container.can_pull(): 117 | return self.system_image_container.image_name() 118 | else: 119 | return "-" 120 | -------------------------------------------------------------------------------- /emu/cloud_build.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import itertools 15 | import logging 16 | import os 17 | import re 18 | import subprocess 19 | from pathlib import Path 20 | 21 | import yaml 22 | 23 | import emu.emu_downloads_menu as emu_downloads_menu 24 | from emu.containers.emulator_container import EmulatorContainer 25 | from emu.containers.system_image_container import SystemImageContainer 26 | from emu.emu_downloads_menu import accept_licenses 27 | from emu.template_writer import TemplateWriter 28 | 29 | 30 | def mkdir_p(path): 31 | """Make directories recursively if path not exists.""" 32 | if not os.path.exists(path): 33 | os.makedirs(path) 34 | 35 | 36 | def git_commit_and_push(dest): 37 | """Commit and pushes this cloud build to the git repo. 38 | 39 | Note that this can be *EXTREMELY* slow as you will likely 40 | have very large objects in your repo. 41 | 42 | Args: 43 | dest ({string}): The destination of the git repository. 44 | """ 45 | subprocess.check_call(["git", "add", "--verbose", "*"], cwd=dest) 46 | subprocess.check_call(["git", "commit", "-F", "README.MD"], cwd=dest) 47 | subprocess.check_call(["git", "push"], cwd=dest) 48 | 49 | 50 | def create_build_step(for_container, destination): 51 | build_destination = Path(destination) / for_container.image_name() 52 | logging.info("Generating %s", build_destination) 53 | for_container.write(build_destination) 54 | if for_container.can_pull(): 55 | logging.warning("Container already available, no need to create step.") 56 | return {} 57 | 58 | step = for_container.create_cloud_build_step(for_container.image_name()) 59 | step["waitFor"] = ["-"] 60 | step["id"] = for_container.image_name() 61 | logging.info("Adding step: %s", step) 62 | return step 63 | 64 | 65 | def cloud_build(args): 66 | """Prepares the cloud build yaml and all its dependencies. 67 | 68 | The cloud builder will generate a single cloudbuild.yaml and generates the build 69 | scripts for every individual container. 70 | 71 | It will construct the proper dependencies as needed. 72 | """ 73 | accept_licenses(True) 74 | 75 | mkdir_p(args.dest) 76 | image_zip = [args.img] 77 | 78 | # Check if we are building a custom image from a zip file 79 | if not os.path.exists(image_zip[0]): 80 | # We are using a standard image, we likely won't need to download it. 81 | image_zip = emu_downloads_menu.find_image(image_zip[0]) 82 | 83 | emulator_zip = [args.emuzip] 84 | if emulator_zip[0] in ["stable", "canary", "all"]: 85 | emulator_zip = [x.download() for x in emu_downloads_menu.find_emulator(emulator_zip[0])] 86 | elif re.match(r"\d+", emulator_zip[0]): 87 | # We must be looking for a build id 88 | logging.warning("Treating %s as a build id", emulator_zip[0]) 89 | emulator_zip = [emu_downloads_menu.download_build(emulator_zip[0])] 90 | 91 | steps = [] 92 | images = [] 93 | emulators = set() 94 | emulator_images = [] 95 | 96 | for (img, emu) in itertools.product(image_zip, emulator_zip): 97 | logging.info("Processing %s, %s", img, emu) 98 | system_container = SystemImageContainer(img, args.repo) 99 | if args.sys: 100 | steps.append(create_build_step(system_container, args.dest)) 101 | else: 102 | for metrics in [True, False]: 103 | emulator_container = EmulatorContainer(emu, system_container, args.repo, metrics) 104 | emulators.add(emulator_container.props["emu_build_id"]) 105 | steps.append(create_build_step(emulator_container, args.dest)) 106 | images.append(emulator_container.full_name()) 107 | emulator_images.append(emulator_container.full_name()) 108 | 109 | cloudbuild = {"steps": steps, "images": images, "timeout": "21600s"} 110 | logging.info("Writing cloud yaml [%s] in %s", yaml, args.dest) 111 | with open(os.path.join(args.dest, "cloudbuild.yaml"), "w", encoding="utf-8") as ymlfile: 112 | yaml.dump(cloudbuild, ymlfile) 113 | 114 | writer = TemplateWriter(args.dest) 115 | writer.write_template( 116 | "cloudbuild.README.MD", 117 | {"emu_version": ", ".join(emulators), 118 | "emu_images": "\n".join([f"* {x}" for x in emulator_images])}, 119 | rename_as="README.MD", 120 | ) 121 | writer.write_template( 122 | "registry.README.MD", 123 | { 124 | "emu_version": ", ".join(emulators), 125 | "emu_images": "\n".join([f"* {x}" for x in images]), 126 | "first_image": next(iter(images), None), 127 | }, 128 | rename_as="REGISTRY.MD", 129 | ) 130 | 131 | if args.git: 132 | git_commit_and_push(args.dest) 133 | -------------------------------------------------------------------------------- /js/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /js/docker/envoy.yaml: -------------------------------------------------------------------------------- 1 | static_resources: 2 | listeners: 3 | - name: development 4 | address: 5 | socket_address: { address: 0.0.0.0, port_value: 8080 } 6 | filter_chains: 7 | - filters: 8 | - name: envoy.filters.network.http_connection_manager 9 | typed_config: 10 | "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager 11 | codec_type: auto 12 | stat_prefix: ingress_http 13 | access_log: 14 | - name: envoy.access_loggers.stdout 15 | typed_config: 16 | "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog 17 | route_config: 18 | name: local_route 19 | virtual_hosts: 20 | - name: local_service 21 | domains: ["*"] 22 | routes: 23 | - match: 24 | prefix: "/android.emulation.control.Rtc" 25 | route: 26 | cluster: emulator_service_grpc 27 | timeout: 0s 28 | max_stream_duration: 29 | grpc_timeout_header_max: 0s 30 | - match: 31 | prefix: "/android.emulation.control" 32 | route: 33 | cluster: emulator_service_grpc 34 | timeout: 0s 35 | max_stream_duration: 36 | grpc_timeout_header_max: 0s 37 | - match: 38 | prefix: "/" 39 | route: 40 | cluster: nginx 41 | cors: 42 | allow_origin_string_match: 43 | - safe_regex: 44 | google_re2: {} 45 | regex: ".*" 46 | allow_methods: GET, PUT, DELETE, POST, OPTIONS 47 | allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout,authorization 48 | max_age: "1728000" 49 | expose_headers: custom-header-1,grpc-status,grpc-message 50 | http_filters: 51 | - name: envoy.filters.http.cors 52 | typed_config: 53 | "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors 54 | - name: envoy.filters.http.jwt_authn 55 | typed_config: 56 | "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication 57 | providers: 58 | firebase_jwt: 59 | issuer: https://securetoken.google.com/android-emulator-webrtc-demo 60 | audiences: 61 | - android-emulator-webrtc-demo 62 | remote_jwks: 63 | http_uri: 64 | uri: https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com 65 | cluster: jwks_cluster 66 | timeout: 60s 67 | cache_duration: 68 | seconds: 300 69 | rules: 70 | - match: 71 | prefix: "/android.emulation.control" 72 | requires: 73 | provider_name: "firebase_jwt" 74 | - match: 75 | prefix: "/android.emulation.control.Rtc" 76 | requires: 77 | provider_name: "firebase_jwt" 78 | - name: envoy.filters.http.grpc_web 79 | typed_config: 80 | "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb 81 | - name: envoy.filters.http.router 82 | typed_config: 83 | "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router 84 | clusters: 85 | - name: jwks_cluster 86 | connect_timeout: 100s 87 | dns_lookup_family: V4_ONLY 88 | type: strict_dns 89 | lb_policy: round_robin 90 | load_assignment: 91 | cluster_name: jwks_cluster 92 | endpoints: 93 | - lb_endpoints: 94 | - endpoint: 95 | address: 96 | socket_address: 97 | address: googleapis.com 98 | port_value: 443 99 | transport_socket: 100 | name: envoy.transport_sockets.tls 101 | typed_config: 102 | "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext 103 | sni: googleapis.com 104 | - name: emulator_service_grpc 105 | connect_timeout: 0.250s 106 | type: STRICT_DNS 107 | lb_policy: ROUND_ROBIN 108 | http2_protocol_options: {} 109 | load_assignment: 110 | cluster_name: emulator_service_grpc 111 | endpoints: 112 | - lb_endpoints: 113 | - endpoint: 114 | address: 115 | socket_address: 116 | address: emulator 117 | port_value: 8554 118 | - name: nginx 119 | connect_timeout: 0.25s 120 | type: STRICT_DNS 121 | lb_policy: ROUND_ROBIN 122 | load_assignment: 123 | cluster_name: nginx 124 | endpoints: 125 | - lb_endpoints: 126 | - endpoint: 127 | address: 128 | socket_address: 129 | address: nginx 130 | port_value: 80 131 | admin: 132 | access_log_path: "/dev/stdout" 133 | address: 134 | socket_address: 135 | address: 0.0.0.0 136 | port_value: 8001 137 | -------------------------------------------------------------------------------- /js/develop/envoy.yaml: -------------------------------------------------------------------------------- 1 | static_resources: 2 | listeners: 3 | - name: development 4 | address: 5 | socket_address: { address: 0.0.0.0, port_value: 8080 } 6 | filter_chains: 7 | - filters: 8 | - name: envoy.filters.network.http_connection_manager 9 | typed_config: 10 | "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager 11 | codec_type: auto 12 | stat_prefix: ingress_http 13 | access_log: 14 | - name: envoy.access_loggers.stdout 15 | typed_config: 16 | "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog 17 | route_config: 18 | name: local_route 19 | virtual_hosts: 20 | - name: local_service 21 | domains: ["*"] 22 | routes: 23 | - match: 24 | prefix: "/android.emulation.control.Rtc" 25 | route: 26 | cluster: emulator_service_grpc 27 | timeout: 0s 28 | max_stream_duration: 29 | grpc_timeout_header_max: 0s 30 | - match: 31 | prefix: "/android.emulation.control.EmulatorController" 32 | route: 33 | cluster: emulator_service_grpc 34 | timeout: 0s 35 | max_stream_duration: 36 | grpc_timeout_header_max: 0s 37 | - match: 38 | prefix: "/" 39 | route: 40 | cluster: nginx 41 | cors: 42 | allow_origin_string_match: 43 | - safe_regex: 44 | google_re2: {} 45 | regex: ".*" 46 | allow_methods: GET, PUT, DELETE, POST, OPTIONS 47 | allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout,authorization 48 | max_age: "1728000" 49 | expose_headers: custom-header-1,grpc-status,grpc-message 50 | http_filters: 51 | - name: envoy.filters.http.cors 52 | typed_config: 53 | "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors 54 | - name: envoy.filters.http.jwt_authn 55 | typed_config: 56 | "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication 57 | providers: 58 | firebase_jwt: 59 | issuer: https://securetoken.google.com/android-emulator-webrtc-demo 60 | audiences: 61 | - android-emulator-webrtc-demo 62 | remote_jwks: 63 | http_uri: 64 | uri: https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com 65 | cluster: jwks_cluster 66 | timeout: 60s 67 | cache_duration: 68 | seconds: 300 69 | rules: 70 | - match: 71 | prefix: "/android.emulation.control" 72 | requires: 73 | provider_name: "firebase_jwt" 74 | - match: 75 | prefix: "/android.emulation.control.Rtc" 76 | requires: 77 | provider_name: "firebase_jwt" 78 | - name: envoy.filters.http.grpc_web 79 | typed_config: 80 | "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb 81 | - name: envoy.filters.http.router 82 | typed_config: 83 | "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router 84 | clusters: 85 | - name: jwks_cluster 86 | connect_timeout: 100s 87 | dns_lookup_family: V4_ONLY 88 | type: strict_dns 89 | lb_policy: round_robin 90 | load_assignment: 91 | cluster_name: jwks_cluster 92 | endpoints: 93 | - lb_endpoints: 94 | - endpoint: 95 | address: 96 | socket_address: 97 | address: googleapis.com 98 | port_value: 443 99 | transport_socket: 100 | name: envoy.transport_sockets.tls 101 | typed_config: 102 | "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext 103 | sni: googleapis.com 104 | - name: emulator_service_grpc 105 | connect_timeout: 0.250s 106 | type: STRICT_DNS 107 | lb_policy: ROUND_ROBIN 108 | http2_protocol_options: {} 109 | load_assignment: 110 | cluster_name: emulator_service_grpc 111 | endpoints: 112 | - lb_endpoints: 113 | - endpoint: 114 | address: 115 | socket_address: 116 | address: emulator 117 | port_value: 8554 118 | - name: nginx 119 | connect_timeout: 0.25s 120 | type: STRICT_DNS 121 | lb_policy: ROUND_ROBIN 122 | load_assignment: 123 | cluster_name: nginx 124 | endpoints: 125 | - lb_endpoints: 126 | - endpoint: 127 | address: 128 | socket_address: 129 | address: nginx 130 | port_value: 80 131 | admin: 132 | access_log_path: "/dev/stdout" 133 | address: 134 | socket_address: 135 | address: 0.0.0.0 136 | port_value: 8001 137 | -------------------------------------------------------------------------------- /cloud-init/cloud-init: -------------------------------------------------------------------------------- 1 | ## template: jinja 2 | #cloud-config 3 | 4 | # Make sure we have a kvm group. 5 | groups: 6 | - kvm 7 | 8 | users: 9 | - name: aemu 10 | uid: 2000 11 | groups: kvm 12 | 13 | write_files: 14 | - path: /etc/udev/rules.d/kvm-permissions 15 | permissions: 0644 16 | owner: root 17 | content: | 18 | KERNEL=="kvm", GROUP="kvm", MODE="0660" 19 | 20 | - path: /run/metadata/aemu 21 | permissions: 0644 22 | owner: root 23 | content: | 24 | INSTANCE_COUNT=1 25 | GRPC_PORT=8554 26 | ADB_PORT=5555 27 | # Replace with your own turn command (Be careful of escaping ") 28 | TURN= 29 | # Additional emulator parameters 30 | EMULATOR_PARAMS= 31 | # Additional avd config 32 | AVD_CONFIG= 33 | # Replace with your own accessible image. 34 | IMAGE=us-docker.pkg.dev/android-emulator-268719/images/30-google-x64:latest 35 | # Replace with your own private adb key 36 | ADBKEY="-----BEGIN PRIVATE KEY----- \ 37 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5k0vjoBVDYb/i \ 38 | dI491H2/cBsSm1YLEVV10J7XOYuUM+83pkn1eYr05++U235hVchjqTuxih/DObHE \ 39 | WoIuQNxGpKQ0jsQBLHf1zvqtBW0cnxLRCq5Lkuk5mo21EC++6pfZjAQaPoPOWGUn \ 40 | mxddLeMbYvJdRh1wjYGDXxZ4pw5GqqY0h9CS6UC4hHOjgAc+tb9ogjRnVoTTeOpr \ 41 | Y2y5DjNgfgrzLq4o8989AGg1BsrirrCYvK1DjkWD+dvgXo14yJKW9Wr7dTeklQhn \ 42 | GzEsSpyoJFG9XNRNfpP4u1+qoRz4UTfgS7q/uVq8HSdghTm4USuwEUQFyCwizeBY \ 43 | BqqTX0FRAgMBAAECggEABUolHNCVhJN85seB3bIwsKgRVHwiJWv9wK9E9M9wiqWZ \ 44 | ky9xrp4jm2ggTpDcWwnI/cq/T2fnkH70scsu6GK7yKV1IwSiAoLOC08WWv/TaLBB \ 45 | 1vywA8pU7Kn6sbNbugxZrlc473bSVtuTDBBGF+dIwMFG9WjMCmcVLs2DQGaKIBJd \ 46 | GYJvSh76LTFE2faPlxClJtdQoX9d0sBPZDkQxVttvA/nIaXOYF1LgFCMxyH6T6jp \ 47 | Iu/xxE8XNudGCxmAS2dMjywb6lLOmPCiwljFcThkyMh2U36DvCnTpf/QCq++Grz7 \ 48 | iOmgVIZRzUDg5VBqJjtt/DyGblS4y7Z1yZpeVnBEQQKBgQDocjWcFTSpaEmyEWQi \ 49 | oID9DN8PUW8aoxkYYNwaPU0g+UIo3l1DTegGtg6gqc4HAXJhhYAMxzqrSqeoYDXD \ 50 | xR9tl2+vDpbt+BJNXQgNAg/0HgBA/Im8M+w3GIsHtsBDVQWaG0UBV9+EfZIIgfd6 \ 51 | dWvv7zwCN77GI7QOakQr87xdEQKBgQDMYTta+7HTnRUW92tlFaxOSWGXjp6pkATO \ 52 | XLl2czIx2ML4SJOTGuI0NZHY2rrmuHp+QUel1pzmEWjjfrbcpMDZWxOaCL4g3tmA \ 53 | VBkdQ0C/WYjrtyjMckM9PBRqfMtJT7BWeuzgQMhPmcdw084MvM7HxvbTfWs8iJpa \ 54 | 9eI6RFGgQQKBgF0/q/gAnc60MpRH28b0YqqhZj6r6YljEqcv/DxeiTmIJR1mDz33 \ 55 | 2/QNRxL269rtnqg2uSbnKccbvOSULB1sT+5UCQ7OKIgws47rmlY1lJbXDj0D0nF4 \ 56 | 1vNHWkbu7nRUgFnRRL6ENPvesB3Pnas3veRUMdul51dvbUU3JkAHmHIxAoGAHzRB \ 57 | MbT4A40aKTWBah+S/SjrA4683rqkYT V7A4C3CzFDI1FBZtZV7w62w9sxagSEfz5M \ 58 | SB+qON4zm3g/RxTIdOcY6Q2oqbAcmSE97F/WRODQrNx8GCrh5TmFDHUdPIY0MB/4 \ 59 | hoydiLm735gW/47cK1hPWx7s/oMEvhqIfcjshYECgYBYye6rOe00jYk9tdLD6bEN \ 60 | PDSJtPu0un3kl9mnS/i+TZ8y4FWQP3Z3Ya2YsU09FE46CbdtaNCleOWk8gTrlPd4 \ 61 | WBrShKReUz8myoiEGKznWz7+fjYPBGX4V/tqv+4yBC/ODu6I8ZtAbe0RBA36g7E6 \ 62 | NhBP5MRzqGMp/A3/WvrZQg== \ 63 | -----END PRIVATE KEY-----" 64 | 65 | - path: /usr/local/bin/append_aemu_metadata 66 | permissions: 0755 67 | owner: root 68 | content: | 69 | #/bin/sh 70 | override_metadata() { 71 | # TODO: Add support for other providers. 72 | {% if v1.cloud_name == 'gce' %} 73 | local METADATA_URL="http://metadata.google.internal/computeMetadata/v1/instance/attributes/" 74 | local response=$(curl -f --silent "${METADATA_URL}$1" -H "Metadata-Flavor: Google") 75 | {% endif %} 76 | if [ "$response" ]; then echo $2=$response >> /run/metadata/aemu; fi 77 | } 78 | override_metadata emulator_grpc_port GRPC_PORT 79 | override_metadata emulator_adb_port ADB_PORT 80 | override_metadata emulator_image IMAGE 81 | override_metadata emulator_adbkey ADBKEY 82 | override_metadata emulator_turn TURN 83 | override_metadata emulator_avd_config AVD_CONFIG 84 | override_metadata emulator_instance_count INSTANCE_COUNT 85 | override_metadata emulator_emulator_params EMULATOR_PARAMS 86 | 87 | - path: /usr/local/bin/launch_containers 88 | permissions: 0755 89 | owner: root 90 | content: | 91 | #!/bin/sh 92 | CONTAINER= 93 | for IDX in $(seq 1 $1); do 94 | LGRPC_PORT=$(($GRPC_PORT + $IDX - 1)) 95 | LADB_PORT=$(($ADB_PORT + $IDX - 1)) 96 | NAME=aemu_$IDX 97 | /usr/bin/docker run --rm -d \ 98 | --name=$NAME \ 99 | --device /dev/kvm \ 100 | -e ADBKEY \ 101 | -e TURN \ 102 | -e EMULATOR_PARAMS \ 103 | -e AVD_CONFIG \ 104 | -p ${LGRPC_PORT}:8554/tcp \ 105 | -p ${LADB_PORT}:5555/tcp \ 106 | --mount type=tmpfs,destination=/data \ 107 | ${IMAGE} 108 | CONTAINER="${CONTAINER} ${NAME}" 109 | done 110 | /usr/bin/docker wait $CONTAINER 111 | 112 | - path: /usr/local/bin/stop_containers 113 | permissions: 0755 114 | owner: root 115 | content: | 116 | #!/bin/sh 117 | /usr/bin/docker container stop $(/usr/bin/docker container ls -q --filter name="aemu") 118 | 119 | - path: /etc/systemd/system/aemu.service 120 | permissions: 0644 121 | owner: root 122 | content: | 123 | [Unit] 124 | Description=Starts the android emulator as a docker service. 125 | Requires=docker.service 126 | After=docker.service 127 | [Service] 128 | EnvironmentFile=/run/metadata/aemu 129 | ExecStart=/usr/local/bin/launch_containers $INSTANCE_COUNT 130 | ExecStop=/usr/local/bin/stop_containers 131 | runcmd: 132 | - chown root:kvm /dev/kvm 133 | - chmod 0660 /dev/kvm 134 | - /usr/local/bin/append_aemu_metadata 135 | - systemctl daemon-reload 136 | - systemctl start aemu.service 137 | -------------------------------------------------------------------------------- /cloud-init/README.MD: -------------------------------------------------------------------------------- 1 | # Android Emulator in the cloud 2 | 3 | [Cloud-init](https://cloudinit.readthedocs.io/en/latest/) is a cross-platform cloud instance initialization standard that is widely supported. In this directory you will find a cloud-init scripts that can be used to configure a cloud instance with a running emulator. 4 | 5 | # Requirements 6 | 7 | You must run a base instance which has KVM and docker available. Details on how to get access to KVM on the various cloud providers can be found here: 8 | 9 | - AWS provides [bare metal](https://aws.amazon.com/about-aws/whats-new/2019/02/introducing-five-new-amazon-ec2-bare-metal-instances/) instances that provide access to KVM. 10 | - Azure: Follow these [instructions](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/nested-virtualization) to enable nested virtualization. 11 | - GCE: Follow these [instructions](https://cloud.google.com/compute/docs/instances/enable-nested-virtualization-vm-instances) to enable nested virtualization. 12 | 13 | _NOTE_ You cannot use GCE's container optimized OS, as it does not make /dev/kvm available. You can follow the steps at the bottom to create you own 14 | version of a container optimized os with KVM enabled. 15 | 16 | Follow the steps to for your cloud provider to create an image with: 17 | 18 | - nested virtualization 19 | - docker 20 | - cloud-init 21 | - tmpfs with at least 8gb per emulator that is launched. 22 | 23 | # Overview 24 | 25 | The cloud-init file will launch a systemd service that will pull a public docker image and launch it. The systemd service will pull in the configuration file `/run/metadata/aemu` which contains a set of properties that modify the behavior of the emulator. 26 | 27 | Most notable are: 28 | 29 | - **GRPC_PORT**: The port where the first instance of the gRPC service will be running. Defaults to 8554. The second instance will be available at 8555, etc. 30 | - **ADB_PORT**: The port where the first instance of the ADB will be available. Defaults to 5555. The second instance will be available at 5556, etc. 31 | - **INSTANCE_COUNT**: The number of emulators to start, defaults to 1. By default the emulator uses 4 vcpus, and 12gb per instance (4 gb memory, 8 gb tmpfs). 32 | - **TURN**: Configuration used to start turn server. 33 | - **ADBKEY**: The private adb key that will be embedded in the emulator. 34 | - **EMULATOR_IMG**: The url to a public docker image that will be launched. Defaults to `us-docker.pkg.dev/android-emulator-268719/images/30-google-x64:latest` 35 | - **AVD_CONFIG**: Additional avd configuration that should be added to the configuration. These parameters will be added to the config.ini of the launched avd. 36 | - **EMULATOR_PARAMS**: Additional emulator parameters that should be added. These parameters are added to the launch of the emulator executable. 37 | 38 | First make sure the [cloud-init](cloud-init) file contains all the proper definitions needed for launch. 39 | 40 | If you are running in gce you have the option the specify the properties above as metadata on the instance: 41 | 42 | - emulator_grpc_port maps to GRPC_PORT 43 | - emulator_adb_port maps to ADB_PORT 44 | - emulator_image maps to $IMAGE 45 | - emulator_adbkey maps to ADBKEY 46 | - emulator_turn maps to TURN 47 | - emulator_instance_count maps to $INSTANCE_COUNT 48 | 49 | Emulators are launched using the aemu.service systemd service. This service will start a series of emulators that are named aemu_1, aemu_2, ..., aemu_n 50 | Keep in mind that it can take a while to pull and launch the emulator, esp. if you are running more than one emulator. 51 | 52 | # Lauching the instance 53 | 54 | For example if you created a gce image with nested virtualization you can launch an instance as follows: 55 | 56 | 57 | ```sh 58 | gcloud compute instances create aemu-example \ 59 | --zone us-west1-b \ 60 | --min-cpu-platform "Intel Haswell" \ 61 | --image cos-dev-nested \ 62 | --machine-type n1-highcpu-32 \ 63 | --tags=http-server,https-server \ 64 | --metadata-from-file user-data=cloud-init \ 65 | --metadata=emulator_adbkey="$(cat ~/.android/adbkey)",emulator_adb_port=80,emulator_grpc_port=443 66 | ``` 67 | 68 | Next you can connect to the emulator from your local machine as follows: 69 | 70 | ```sh 71 | IP=$(gcloud compute instances describe aemu-example --format='get(networkInterfaces[0].accessConfigs[0].natIP)`) 72 | adb connect $IP:80 73 | ``` 74 | 75 | Your device should now be available for access over adb. 76 | 77 | ## Building a container optimized os (cos) with kvm enabled. 78 | 79 | Google provides a container optimized os for running docker images. Unfortunately the base images do not expose the 80 | kvm kernels and cannot be used directly for the android emulator. In order to run them on gce you will have to 81 | build your own cos image, with KVM enabled. 82 | 83 | - First obtain the [cos source](https://cloud.google.com/container-optimized-os/docs/how-to/building-from-open-source#obtaining_the_source_code) 84 | - Next apply the following changes: 85 | 86 | ``` 87 | diff --git a/project-lakitu/sys-kernel/lakitu-kernel-5_4/files/base.config b/project-lakitu/sys-kernel/lakitu-kernel-5_4/files/base.config 88 | index e13a9e170e..a31b2b73db 100644 89 | --- a/project-lakitu/sys-kernel/lakitu-kernel-5_4/files/base.config 90 | +++ b/project-lakitu/sys-kernel/lakitu-kernel-5_4/files/base.config 91 | @@ -609,7 +609,13 @@ CONFIG_EFI_EARLYCON=y 92 | 93 | CONFIG_HAVE_KVM=y 94 | CONFIG_VIRTUALIZATION=y 95 | -# CONFIG_KVM is not set 96 | +# is not set 97 | +CONFIG_KVM=m 98 | +CONFIG_KVM_INTEL=m 99 | +CONFIG_KVM_MMU_AUDIT=m 100 | +CONFIG_KVM_AMD=m 101 | +CONFIG_VHOST_NET=m 102 | +CONFIG_VHOST_VSOCK=m 103 | # CONFIG_VHOST_NET is not set 104 | # CONFIG_VHOST_SCSI is not set 105 | # CONFIG_VHOST_VSOCK is not set 106 | ``` 107 | 108 | in ./src/overlays 109 | 110 | - Next follow the steps to build to [os](https://cloud.google.com/container-optimized-os/docs/how-to/building-from-open-source#building_a_image) 111 | - Follow the steps to import the build into a system image in [gce](https://cloud.google.com/container-optimized-os/docs/how-to/building-from-open-source#running_on) 112 | 113 | Next we need to create an image with nested virtualizaton enabled. In the steps below we assume that you imported the image as `emu-dev-cos-base` is `us-west1-b`: 114 | 115 | gcloud compute disks create cos-dev-nested-disk --image emu-dev-cos-base --zone us-west1-b 116 | gcloud compute images create cos-dev-nested --source-disk cos-dev-nested-disk --source-disk-zone us-west1-b --licenses "https://www.googleapis.com/compute/v1/projects/vm-options/global/licenses/enable-vmx" 117 | 118 | Congratulations! You have created a `cos-dev-nested` image with virtualization enabled that can be used in the examples above. 119 | 120 | 121 | -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | JavaScript WebRTC samples 2 | ========================= 3 | 4 | This document descibes how to run the gRPC/WebRTC example. Support for WebRTC is officially only available on linux releases. 5 | This sample expects the emulator to be running in a server like environment: 6 | 7 | - There is a Webserver hosting the HTML/JS 8 | - There is a [gRPC web proxy](https://grpc.io/blog/state-of-grpc-web/) 9 | - The udp ports required for WebRTC are open, or a turn service is configured. 10 | 11 | These services should be accessible for the browsers that want to interact with the emulator. For example a publicly visible GCE/AWS server should work fine. 12 | 13 | This sample is based on ReactJS and provides the following set of components: 14 | 15 | - Emulator: This component displays the emulator and will send mouse & keyboard events to it. 16 | - LogcatView: A view that displays the current output of logcat. This currently relies on the material-ui theme. 17 | 18 | Both components require the following properties to be set: 19 | 20 | - `emulator`: This property must contain an `EmulatorControllerService` object. 21 | 22 | You will likely need to modify `App.js` and `index.html` to suit your needs. 23 | 24 | - There is a Webserver hosting the HTML/JS, we are using [NodeJs](https://nodejs.org/en/) for development. 25 | - We are using [ReactJS](https://reactjs.org/) as our component framework. 26 | - We use [Envoy](https://www.envoyproxy.io/) as the [gRPC web proxy](https://grpc.io/blog/state-of-grpc-web/). 27 | - You are running [containerized](../README.MD) version of the emulator. 28 | 29 | For fast video over [WebRTC](www.webrtc.org): 30 | 31 | - You are using linux. 32 | - You have android sdk installed, and the environment variable `ANDROID_SDK_ROOT` is set properly. The easiest way to install the sdk is by installing [Android Studio](https://developer.android.com/studio/install). 33 | - An emulator build newer than 5769853. You can either: 34 | - Check if your current installed version will work. Run: 35 | ```sh 36 | $ $ANDROID_SDK_ROOT/emulator/emulator -version | head -n 1 37 | ``` 38 | and make sure that the reported build_id is higher than 5769853 39 | - Build one from source yourself. 40 | - Obtain one from the [build bots](http://go/ab/emu-master-dev). Make sure to get sdk-repo-linux-emulator-XXXX.zip where XXXX is the build number. You can unzip the contents to `$ANDROID_SDK_ROOT`. For example: 41 | ```sh 42 | $ unzip ~/Downloads/sdk-repo-linux-emulator-5775474.zip -d $ANDROID_SDK_ROOT 43 | ``` 44 | - A valid virtual device to run inside the emulator. Instructions on how to create a virtual device can be found [here](https://developer.android.com/studio/run/managing-avds). Any virtual device can be used. 45 | - [Node.js](https://nodejs.org/en/) Stable version 10.16.1 LTS or later. 46 | - A [protobuf](https://developers.google.com/protocol-buffers/) compiler, version 3.6 or higher is supported. 47 | - [Docker](https://www.docker.com). We will use the container infrastructure for easy deployment. Follow the instructions [here](http://go/installdocker) if you are within Google. 48 | 49 | 50 | # Configure the emulator 51 | 52 | Make sure you are able to launch the emulator from the command line with a valid avd. Instructions on how to create a virtual device can be found [here](https://developer.android.com/studio/run/managing-avds). 53 | 54 | For example if you created a avd with the name P, you should be able to launch it as follows: 55 | 56 | ```sh 57 | $ $ANDROID_SDK_ROOT/emulator/emulator @P 58 | ``` 59 | 60 | Make sure that the emulator is working properly, and that can use the desired avd. 61 | 62 | WebRTC support will be activated if the emulator is launched with the `-grpc ` flag. The current demos expect the gRPC endpoint to be available at `localhost:8554`. This port only needs to be accessible by the gRPC proxy that is being used. There is no need for this port to be publicly visible. 63 | 64 | ```sh 65 | $ $ANDROID_SDK_ROOT/emulator/emulator @P -grpc 8554 66 | ``` 67 | 68 | ### Do I need TURN? 69 | 70 | The most important thing to is to figure out if you need a [Turn Server](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT). 71 | **You usually only need this if your server running the emulator is behind a firewall, and not publicly accessible.** 72 | Most of the time there is no need for a turn server. If you do have needs for a turn server you can follow the steps in the 73 | [README](turn/README.MD). 74 | 75 | # Internal Organization 76 | 77 | This sample is based on ReactJS and uses the android-emulator-webrtc module to display the emulator. 78 | 79 | - EmulatorScreen: This component displays the emulator and will send mouse & keyboard events to it. 80 | - LogcatView: A view that displays the current output of logcat. This currently relies on the material-ui theme. 81 | 82 | Both components require the following properties to be set: 83 | 84 | - `uri`: This property must contain a URI to the gRPC proxy. 85 | - `auth`: This property must contain an AuthService that implements the following two methods: 86 | - `authHeader()` which must return a set of headers that should be send along with a request. For example: 87 | ```js 88 | return { Authorization: "token header" }; 89 | ``` 90 | - `unauthorized()` a function that gets called when a 401 was received, here you can implement logic 91 | to make sure the next set of headers contain what is needed. 92 | 93 | Components that you will need to include: 94 | - **TokenAuthService**: An authentication service that provides a JWT token to the emulator. 95 | 96 | You will likely need to modify `App.js` and `index.html` to suit your needs. 97 | 98 | ## As a Developer 99 | 100 | As a developer you will make use of an envoy docker container and use node.js to serve the react app. First you must 101 | make sure you create a containerized version of the emulator as follows: 102 | 103 | ```sh 104 | emu-docker create stable "Q google_apis_playstore x86" 105 | ``` 106 | 107 | This will binplace all the files needed for development under the src directory. 108 | Next you can get the development environment ready by: 109 | 110 | 111 | ```sh 112 | $ make deps 113 | ``` 114 | 115 | And start envoy + nodejs as follows: 116 | 117 | ``` 118 | $ make develop 119 | ``` 120 | 121 | This should open up a browser, and detect any change made to the webpages and JavaScript sources. Hit ctrl-c to stop the dev environment. Note that shutdown takes a bit as a docker container needs to shut down. 122 | 123 | ## Limitations 124 | 125 | gRPC is not well supported in the browser and has only support for unary calls and server side streaming (when using envoy). This restricts support for other services such as [Waterfall](https://github.com/google/devx-tools/tree/master/waterfall). 126 | -------------------------------------------------------------------------------- /emu/templates/launch-emulator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2019 - The Android Open Source Project 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | VERBOSE=3 17 | ANDROID_AVD_HOME=/root/.android/avd 18 | 19 | is_mounted () { 20 | mount | grep "$1" 21 | } 22 | 23 | # Run a command, output depends on verbosity level 24 | run () { 25 | if [ "$VERBOSE" -lt 0 ]; then 26 | VERBOSE=0 27 | fi 28 | if [ "$VERBOSE" -gt 1 ]; then 29 | echo "COMMAND: $@" 30 | fi 31 | case $VERBOSE in 32 | 0|1) 33 | eval "$@" >/dev/null 2>&1 34 | ;; 35 | 2) 36 | eval "$@" >/dev/null 37 | ;; 38 | *) 39 | eval "$@" 40 | ;; 41 | esac 42 | } 43 | 44 | 45 | log_version_info() { 46 | # This function logs version info. 47 | emulator/emulator -version | head -n 1 | sed -u 's/^/version: /g' 48 | echo 'version: launch_script: {{version}}' 49 | img=$ANDROID_SDK_ROOT/system-images/android 50 | [ -f "$img/x86_64/source.properties" ] && cat "$img/x86_64/source.properties" | sed -u 's/^/version: /g' 51 | [ -f "$img/x86/source.properties" ] && cat "$img/x86/source.properties" | sed -u 's/^/version: /g' 52 | } 53 | 54 | install_adb_keys() { 55 | # We do not want to keep adb secrets around, if the emulator 56 | # ever created the secrets itself we will never be able to connect. 57 | run rm -f /root/.android/adbkey /root/.android/adbkey.pub 58 | 59 | if [ -s "/run/secrets/adbkey" ]; then 60 | echo "emulator: Copying private key from secret partition" 61 | run cp /run/secrets/adbkey /root/.android 62 | elif [ ! -z "${ADBKEY}" ]; then 63 | echo "emulator: Using provided adb private key" 64 | echo "-----BEGIN PRIVATE KEY-----" >/root/.android/adbkey 65 | echo $ADBKEY | tr " " "\\n" | sed -n "4,29p" >>/root/.android/adbkey 66 | echo "-----END PRIVATE KEY-----" >>/root/.android/adbkey 67 | elif [ ! -z "${ADBKEY_PUB}" ]; then 68 | echo "emulator: Using provided adb public key" 69 | echo $ADBKEY_PUB >>/root/.android/adbkey.pub 70 | else 71 | echo "emulator: No adb key provided, creating internal one, you might not be able connect from adb." 72 | run /android/sdk/platform-tools/adb keygen /root/.android/adbkey 73 | fi 74 | run chmod 600 /root/.android/adbkey 75 | 76 | unset ADBKEY 77 | unset ADBKEY_PUB 78 | } 79 | 80 | # Installs the console tokens, if any. The environment variable |TOKEN| will be 81 | # non empty if a token has been set. 82 | install_console_tokens() { 83 | if [ -s "/run/secrets/token" ]; then 84 | echo "emulator: Copying console token from secret partition" 85 | run cp /run/secrets/token /root/.emulator_console_auth_token 86 | TOKEN=yes 87 | elif [ ! -z "${TOKEN}" ]; then 88 | echo "emulator: Using provided emulator console token" 89 | echo ${TOKEN} >/root/.emulator_console_auth_token 90 | else 91 | echo "emulator: No console token provided, console disabled." 92 | fi 93 | 94 | if [ ! -z "${TOKEN}" ]; then 95 | echo "emulator: forwarding the emulator console." 96 | socat -d tcp-listen:5554,reuseaddr,fork tcp:127.0.0.1:5556 & 97 | fi 98 | 99 | unset TOKEN 100 | } 101 | 102 | install_grpc_certs() { 103 | # Copy certs if they exists and are not empty. 104 | [ -s "/run/secrets/grpc_cer" ] && cp /run/secrets/grpc_cer /root/.android/emulator-grpc.cer 105 | [ -s "/run/secrets/grpc_key" ] && cp /run/secrets/grpc_key /root/.android/emulator-grpc.key 106 | } 107 | 108 | clean_up() { 109 | # Delete any leftovers from hard exits. 110 | run rm -rf /tmp/* 111 | run rm -rf ${ANDROID_AVD_HOME}/Pixel2.avd/*.lock 112 | 113 | # Check for core-dumps, that might be left over 114 | if ls core* 1>/dev/null 2>&1; then 115 | echo "emulator: ** WARNING ** WARNING ** WARNING **" 116 | echo "emulator: Core dumps exist in this image. This means the emulator has crashed in the past." 117 | fi 118 | 119 | mkdir -p /root/.android 120 | } 121 | 122 | setup_pulse_audio() { 123 | # We need pulse audio for the webrtc video bridge, let's configure it. 124 | run mkdir -p /root/.config/pulse 125 | export PULSE_SERVER=unix:/tmp/pulse-socket 126 | run pulseaudio -D -vvvv --log-time=1 --log-target=newfile:/tmp/pulseverbose.log --log-time=1 --exit-idle-time=-1 127 | tail -f /tmp/pulseverbose.log -n +1 | sed -u 's/^/pulse: /g' & 128 | run pactl list || exit 1 129 | } 130 | 131 | forward_loggers() { 132 | run mkdir /tmp/android-unknown 133 | run mkfifo /tmp/android-unknown/kernel.log 134 | run mkfifo /tmp/android-unknown/logcat.log 135 | echo "emulator: It is safe to ignore the warnings from tail. The files will come into existence soon." 136 | tail --retry -f /tmp/android-unknown/goldfish_rtc_0 | sed -u 's/^/video: /g' & 137 | cat /tmp/android-unknown/kernel.log | sed -u 's/^/kernel: /g' & 138 | cat /tmp/android-unknown/logcat.log | sed -u 's/^/logcat: /g' & 139 | } 140 | 141 | initialize_data_part() { 142 | # Check if we have mounted a data partition (tmpfs, or persistent) 143 | # and if so, we will use that as our avd directory. 144 | if is_mounted /data; then 145 | run cp -fr /android-home/ /data 146 | ln -sf /data/android-home ${ANDROID_AVD_HOME} 147 | echo "path=${ANDROID_AVD_HOME}/Pixel2.avd" > ${ANDROID_AVD_HOME}/Pixel2.ini 148 | else 149 | ln -sf /android-home ${ANDROID_AVD_HOME} 150 | fi 151 | } 152 | 153 | # Let us log the emulator,script and image version. 154 | log_version_info 155 | initialize_data_part 156 | clean_up 157 | install_console_tokens 158 | install_adb_keys 159 | install_grpc_certs 160 | setup_pulse_audio 161 | forward_loggers 162 | 163 | # Override config settings that the user forcefully wants to override. 164 | if [ ! -z "${AVD_CONFIG}" ]; then 165 | echo "Adding ${AVD_CONFIG} to config.ini" 166 | echo "${AVD_CONFIG}" >>"/root/.android/avd/Pixel2.avd/config.ini" 167 | fi 168 | 169 | # Launch internal adb server, needed for our health check. 170 | # Once we have the grpc status point we can use that instead. 171 | /android/sdk/platform-tools/adb start-server 172 | 173 | # All our ports are loopback devices, so setup a simple forwarder 174 | socat -d tcp-listen:5555,reuseaddr,fork tcp:127.0.0.1:5557 & 175 | 176 | # Basic launcher command, additional flags can be added. 177 | LAUNCH_CMD=("emulator/emulator") 178 | LAUNCH_CMD+=("-avd" "Pixel2") 179 | LAUNCH_CMD+=("-ports" "5556,5557" "-grpc" "8554" "-no-window") 180 | LAUNCH_CMD+=("-skip-adb-auth" "-no-snapshot-save" "-wipe-data" "-no-boot-anim") 181 | LAUNCH_CMD+=("-shell-serial" "file:/tmp/android-unknown/kernel.log") 182 | LAUNCH_CMD+=("-logcat" "*:V") 183 | LAUNCH_CMD+=("-feature" "AllowSnapshotMigration") 184 | LAUNCH_CMD+=("-gpu" "swiftshader_indirect" {{extra}}) 185 | 186 | if [ ! -z "${EMULATOR_PARAMS}" ]; then 187 | LAUNCH_CMD+=($EMULATOR_PARAMS) 188 | fi 189 | 190 | if [ ! -z "${TURN}" ]; then 191 | LAUNCH_CMD+=("-turncfg" "${TURN}") 192 | fi 193 | 194 | # Add qemu specific parameters 195 | LAUNCH_CMD+=("-qemu" "-append" "panic=1") 196 | 197 | if [ ! -z "${ANDROID_AVD_HOME}" ]; then 198 | export ANDROID_AVD_HOME=/android-home 199 | fi 200 | 201 | # Kick off the emulator 202 | echo "emulator: " $(env) 203 | echo "emulator: ${LAUNCH_CMD[@]}" 204 | exec "${LAUNCH_CMD[@]}" 205 | # All done! 206 | -------------------------------------------------------------------------------- /emu/android_release_zip.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import collections 15 | import logging 16 | import os 17 | import shutil 18 | import zipfile 19 | from typing import Dict, Set, Union 20 | 21 | from tqdm import tqdm 22 | 23 | API_LETTER_MAPPING = { 24 | "10": "G", 25 | "15": "I", 26 | "16": "J", 27 | "17": "J", 28 | "18": "J", 29 | "19": "K", 30 | "21": "L", 31 | "22": "L", 32 | "23": "M", 33 | "24": "N", 34 | "25": "N", 35 | "26": "O", 36 | "27": "O", 37 | "28": "P", 38 | "29": "Q", 39 | "30": "R", 40 | "31": "S", 41 | "32": "S", 42 | "33": "T", 43 | } 44 | 45 | 46 | def api_codename(api): 47 | """First letter of the desert, if any.""" 48 | if api in API_LETTER_MAPPING: 49 | return API_LETTER_MAPPING[api] 50 | else: 51 | return "_" 52 | 53 | 54 | class NotAZipfile(Exception): 55 | pass 56 | 57 | 58 | class AndroidReleaseZip(object): 59 | """Provides information of released android products. 60 | 61 | Every released zip file contains a source.properties file, this 62 | source.properties file contains [key]=[value] pairs with information 63 | about the contents of the zip. 64 | """ 65 | 66 | def __init__(self, file_name: str): 67 | self.file_name: str = file_name 68 | if not zipfile.is_zipfile(file_name): 69 | raise NotAZipfile(f"{file_name} is not a zipfile!") 70 | 71 | with zipfile.ZipFile(file_name, "r") as zip_file: 72 | self.props: Dict[str, Set[str]] = collections.defaultdict(set) 73 | files = [ 74 | x 75 | for x in zip_file.infolist() 76 | if "source.properties" in x.filename or "build.prop" in x.filename 77 | ] 78 | for file in files: 79 | for key, value in self._unpack_properties(zip_file, file).items(): 80 | self.props[key] = value 81 | 82 | def _unpack_properties( 83 | self, zip_file: zipfile.ZipFile, zip_info: zipfile.ZipInfo 84 | ) -> Dict[str, str]: 85 | prop = zip_file.read(zip_info).decode("utf-8").splitlines() 86 | res = dict([a.split("=") for a in prop if "=" in a]) 87 | return res 88 | 89 | def __str__(self) -> str: 90 | return f"{self.description()}-{self.revision()}" 91 | 92 | def description(self) -> Union[str, None]: 93 | """Descripton of this release.""" 94 | return self.props.get("Pkg.Desc") 95 | 96 | def revision(self) -> Union[str, None]: 97 | """The revision of this release.""" 98 | return self.props.get("Pkg.Revision") 99 | 100 | def build_id(self) -> str: 101 | """The Pkg.BuildId or revision if build id is not available.""" 102 | if "Pkg.BuildId" in self.props: 103 | return self.props.get("Pkg.BuildId") 104 | return self.revision() 105 | 106 | def is_system_image(self) -> bool: 107 | """True if this zip file contains a system image.""" 108 | return ( 109 | "System Image" in self.description() 110 | or "Android SDK Platform" in self.description() 111 | ) 112 | 113 | def is_emulator(self) -> bool: 114 | """True if this zip file contains the android emulator.""" 115 | return "Android Emulator" in self.description() 116 | 117 | def copy(self, destination: str) -> str: 118 | """Copy the zipfile to the given destination. 119 | 120 | If the destination is the same as this zipfile the current path 121 | will be returned a no copy is made. 122 | 123 | Args: 124 | destination (str): The destination to copy this zip to. 125 | 126 | Returns: 127 | str: The path where this zip file was copied to. 128 | """ 129 | try: 130 | return shutil.copy2(self.file_name, destination) 131 | except shutil.SameFileError: 132 | logging.warning("Will not copy to itself, ignoring..") 133 | return self.file_name 134 | 135 | def extract(self, destination: str) -> None: 136 | """Extract this release zip to the given destination 137 | 138 | Args: 139 | destination (str): The destination to extract the zipfile to. 140 | """ 141 | 142 | zip_file = zipfile.ZipFile(self.file_name) 143 | print(f"Extracting: {self.file_name} -> {destination}") 144 | for info in tqdm(iterable=zip_file.infolist(), total=len(zip_file.infolist())): 145 | filename = zip_file.extract(info, path=destination) 146 | mode = info.external_attr >> 16 147 | if mode: 148 | os.chmod(filename, mode) 149 | 150 | 151 | class SystemImageReleaseZip(AndroidReleaseZip): 152 | """An Android Release Zipfile containing an emulator system image.""" 153 | 154 | ABI_CPU_MAP: Dict[str, str] = { 155 | "armeabi-v7a": "arm", 156 | "arm64-v8a": "arm64", 157 | "x86_64": "x86_64", 158 | "x86": "x86", 159 | } 160 | SHORT_MAP: Dict[str, str] = { 161 | "armeabi-v7a": "a32", 162 | "arm64-v8a": "a64", 163 | "x86_64": "x64", 164 | "x86": "x86", 165 | } 166 | SHORT_TAG: Dict[str, str] = { 167 | "android": "aosp", 168 | "google_apis": "google", 169 | "google_apis_playstore": "playstore", 170 | "google_atd": "google_atd", 171 | "google_ndk_playstore": "ndk_playstore", 172 | "android-tv": "tv", 173 | } 174 | 175 | def __init__(self, file_name: str): 176 | super().__init__(file_name) 177 | if not self.is_system_image(): 178 | raise NotAZipfile(f"{file_name} is not a zip file with a system image") 179 | 180 | self.props["qemu.cpu"] = self.qemu_cpu() 181 | self.props["qemu.tag"] = self.tag() 182 | self.props["qemu.short_tag"] = self.short_tag() 183 | self.props["qemu.short_abi"] = self.short_abi() 184 | 185 | def api(self) -> str: 186 | """The api level, if any.""" 187 | return self.props.get("AndroidVersion.ApiLevel", "") 188 | 189 | def codename(self) -> str: 190 | """First letter of the desert, if any.""" 191 | if "AndroidVersion.CodeName" in self.props: 192 | return self.props["AndroidVersion.CodeName"] 193 | return api_codename(self.api()) 194 | 195 | def abi(self) -> str: 196 | """The abi if any.""" 197 | return self.props.get("SystemImage.Abi", "") 198 | 199 | def short_abi(self) -> str: 200 | """Shortened version of the ABI string.""" 201 | if self.abi() not in self.SHORT_MAP: 202 | logging.error("%s not in short map", self) 203 | return self.SHORT_MAP[self.abi()] 204 | 205 | def qemu_cpu(self) -> str: 206 | """Returns the cpu architecture, derived from the abi.""" 207 | return self.ABI_CPU_MAP.get(self.abi(), "None") 208 | 209 | def gpu(self) -> str: 210 | """Returns whether or not the system has gpu support.""" 211 | return self.props.get("SystemImage.GpuSupport") 212 | 213 | def tag(self) -> str: 214 | """The tag associated with this release.""" 215 | tag = self.props.get("SystemImage.TagId", "") 216 | if tag == "default" or tag.strip() == "": 217 | tag = "android" 218 | return tag 219 | 220 | def short_tag(self) -> str: 221 | """A shorthand tag.""" 222 | return self.SHORT_TAG[self.tag()] 223 | -------------------------------------------------------------------------------- /js/src/components/emulator_screen.js: -------------------------------------------------------------------------------- 1 | import withStyles from '@mui/styles/withStyles'; 2 | 3 | import AppBar from "@mui/material/AppBar"; 4 | import Box from "@mui/material/Box"; 5 | import Container from "@mui/material/Container"; 6 | import Copyright from "./copyright"; 7 | import { Emulator } from "android-emulator-webrtc/emulator"; 8 | import LogcatView from "./logcat_view"; 9 | import ExitToApp from "@mui/icons-material/ExitToApp"; 10 | import Grid from "@mui/material/Grid"; 11 | import IconButton from "@mui/material/IconButton"; 12 | import ImageIcon from "@mui/icons-material/Image"; 13 | import OndemandVideoIcon from "@mui/icons-material/OndemandVideo"; 14 | import Slider from "@mui/material/Slider"; 15 | import VolumeDown from "@mui/icons-material/VolumeDown"; 16 | import VolumeUp from "@mui/icons-material/VolumeUp"; 17 | import LocationOnIcon from "@mui/icons-material/LocationOn"; 18 | import PropTypes from "prop-types"; 19 | import React from "react"; 20 | import Toolbar from "@mui/material/Toolbar"; 21 | import Typography from "@mui/material/Typography"; 22 | import Snackbar from "@mui/material/Snackbar"; 23 | import Alert from '@mui/material/Alert'; 24 | 25 | const styles = (theme) => ({ 26 | root: { 27 | flexGrow: 1, 28 | }, 29 | menuButton: { 30 | marginRight: theme.spacing(2), 31 | }, 32 | title: { 33 | flexGrow: 1, 34 | }, 35 | nofocusborder: { 36 | outline: "none !important;", 37 | }, 38 | paper: { 39 | marginTop: theme.spacing(4), 40 | display: "flex", 41 | flexDirection: "column", 42 | alignItems: "center", 43 | }, 44 | }); 45 | 46 | // We want a white slider, otherwise it will be invisible in the appbar. 47 | const WhiteSlider = withStyles({ 48 | thumb: { 49 | color: "white", 50 | }, 51 | track: { 52 | color: "white", 53 | }, 54 | rail: { 55 | color: "white", 56 | }, 57 | })(Slider); 58 | 59 | // This class is responsible for hosting two emulator components next to each other: 60 | // One the left it will display the emulator, and on the right it will display the 61 | // active logcat. 62 | // 63 | // It uses the material-ui to display a toolbar. 64 | class EmulatorScreen extends React.Component { 65 | state = { 66 | view: "webrtc", 67 | error_snack: false, 68 | error_msg: "", 69 | emuState: "connecting", 70 | muted: true, 71 | volume: 0.0, 72 | hasAudio: false, 73 | // Let's start at the Googleplex 74 | gps: { latitude: 37.4221, longitude: -122.0841 }, 75 | }; 76 | 77 | static propTypes = { 78 | uri: PropTypes.string, // grpc endpoint 79 | auth: PropTypes.object, // auth module to use. 80 | }; 81 | 82 | stateChange = (s) => { 83 | this.setState({ emuState: s }); 84 | }; 85 | 86 | onAudioStateChange = (s) => { 87 | console.log("Received an audio state change from the emulator."); 88 | this.setState({ hasAudio: s }); 89 | }; 90 | 91 | onError = (err) => { 92 | this.setState({ 93 | error_snack: true, 94 | error_msg: "Low level gRPC error: " + JSON.stringify(err), 95 | }); 96 | }; 97 | 98 | updateLocation = () => { 99 | if (navigator.geolocation) { 100 | navigator.geolocation.getCurrentPosition((location) => { 101 | const loc = location.coords; 102 | this.setState({ 103 | gps: { latitude: loc.latitude, longitude: loc.longitude }, 104 | }); 105 | }); 106 | } 107 | }; 108 | 109 | handleClose = (e) => { 110 | this.setState({ error_snack: false }); 111 | }; 112 | 113 | handleVolumeChange = (e, newVolume) => { 114 | const muted = newVolume === 0; 115 | this.setState({ volume: newVolume, muted: muted }); 116 | }; 117 | 118 | render() { 119 | const { uri, auth, classes } = this.props; 120 | const { 121 | view, 122 | emuState, 123 | error_snack, 124 | error_msg, 125 | muted, 126 | volume, 127 | hasAudio, 128 | gps, 129 | } = this.state; 130 | return ( 131 |
132 | 133 | 134 | 135 | Using emulator view: {view} 136 | 137 | 138 | {hasAudio ? ( // Only display volume control if this emulator supports audio. 139 | 140 | 141 | 142 | 143 | 144 | 145 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | ) : ( 160 | // No audio track, so no volume slider. 161 |
162 | )} 163 | 164 |
165 |
166 | 171 | 172 | 173 | this.setState({ view: "webrtc" })} 177 | size="large"> 178 | 179 | 180 | this.setState({ view: "png" })} 184 | size="large"> 185 | 186 | 187 | auth.logout()} 192 | size="large"> 193 | 194 | 195 |
196 | 197 | 198 | 199 |
200 | 201 | 202 | 203 | 217 |

State: {emuState}

218 |
219 |
220 | 221 | 222 | 223 |
224 |
225 | 226 | 227 | 228 | 229 | 230 | {error_msg} 231 | 232 | 233 |
234 | ); 235 | } 236 | } 237 | 238 | export default withStyles(styles)(EmulatorScreen); 239 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | # Known Issues 2 | 3 | Here are a list of things that we have seen with potential workarounds: 4 | 5 | ## Why can't I use an image before O? 6 | 7 | Releases before O are using an older linux kernel (3.10). This version of the 8 | linux kernel has some issues that are under [active 9 | investigation](https://issuetracker.google.com/issues/140881613) for X86_64 images. 10 | 11 | 12 | ## Unable to find emu-docker 13 | 14 | Some people have reported issues around launching `emu-docker`. 15 | [Issue #30](https://github.com/google/android-emulator-container-scripts/issues/30). 16 | The easiest way to resolve this is to run the project in a virtual environment. 17 | The virtual environment can be activated as follows: 18 | 19 | ```sh 20 | source ./configure.sh 21 | ``` 22 | 23 | This will create a virtual environment and activate it for you. This will make 24 | sure everything is isolated and you should be able to launch `emu-docker`. 25 | 26 | ## Exceptions when trying to create the docker container: 27 | 28 | If you see an exception along the following lines: 29 | 30 | ```python 31 | Traceback (most recent call last): 32 | File "/tmp/android-emulator-container-scripts/emu/docker_device.py", line 78, in create_container 33 | logging.info(api_client.version()) 34 | File "/tmp/android-emulator-container-scripts/venv/lib/python3.5/site-packages/docker-4.0.2-py3.5.egg/dn 35 | return self._result(self._get(url), json=True) 36 | File "/tmp/android-emulator-container-scripts/venv/lib/python3.5/site-packages/docker-4.0.2-py3.5.egg/dr 37 | return f(self, *args, **kwargs) 38 | File "/tmp/android-emulator-container-scripts/venv/lib/python3.5/site-packages/docker-4.0.2-py3.5.egg/dt 39 | return self.get(url, **self._set_request_timeout(kwargs)) 40 | File "/tmp/android-emulator-container-scripts/venv/lib/python3.5/site-packages/requests-2.22.0-py3.5.egt 41 | return self.request('GET', url, **kwargs) 42 | File "/tmp/android-emulator-container-scripts/venv/lib/python3.5/site-packages/requests-2.22.0-py3.5.egt 43 | resp = self.send(prep, **send_kwargs) 44 | File "/tmp/android-emulator-container-scripts/venv/lib/python3.5/site-packages/requests-2.22.0-py3.5.egd 45 | r = adapter.send(request, **kwargs) 46 | File "/tmp/android-emulator-container-scripts/venv/lib/python3.5/site-packages/requests-2.22.0-py3.5.egd 47 | raise ConnectionError(err, request=request) 48 | requests.exceptions.ConnectionError: ('Connection aborted.', PermissionError(13, 'Permission denied')) 49 | ``` 50 | This means you do not have permission to interact with docker. You must enable sudoless docker, follow the 51 | steps outlined [here](https://docs.docker.com/install/linux/linux-postinstall/) 52 | 53 | ## Exception around ADB 54 | 55 | If you see an exception along the following lines: 56 | 57 | ```python 58 | Traceback (most recent call last): 59 | File "/tmp/android-emulator-container-scripts/venv/bin/emu-docker", line 11, in 60 | load_entry_point('emu-docker', 'console_scripts', 'emu-docker')() 61 | File "/tmp/android-emulator-container-scripts/emu/emu_docker.py", line 123, in main 62 | args.func(args) 63 | File "/tmp/android-emulator-container-scripts/emu/emu_docker.py", line 48, in create_docker_image_intere 64 | device.create_docker_file(args.extra) 65 | File "/tmp/android-emulator-container-scripts/emu/docker_device.py", line 119, in create_docker_file 66 | raise IOError(errno.ENOENT, "Unable to find ADB below $ANDROID_SDK_ROOT or on the path!") 67 | FileNotFoundError: [Errno 2] Unable to find ADB below $ANDROID_SDK_ROOT or on the path! 68 | ``` 69 | 70 | You will need to install adb. 71 | 72 | ## Exceptions when providing wrong zipfiles 73 | 74 | If you seen an exception along the following lines: 75 | 76 | ```python 77 | Traceback (most recent call last): 78 | File "/tmp/android-emulator-container-scripts/venv/bin/emu-docker", line 11, in 79 | load_entry_point('emu-docker', 'console_scripts', 'emu-docker')() 80 | File "/tmp/android-emulator-container-scripts/emu/emu_docker.py", line 159, in main 81 | args.func(args) 82 | File "/tmp/android-emulator-container-scripts/emu/emu_docker.py", line 44, in create_docker_image 83 | raise Exception("{} is not a zip file with a system image".format(imgzip)) 84 | Exception: emulator-29.2.8.zip is not a zip file with a system image 85 | ``` 86 | 87 | You likely provided the parameters in the incorrect order. 88 | 89 | ## The container suddenly stopped and I cannot restart it. 90 | 91 | It is possible that the emulator crashes or terminates unexpectedly. In this 92 | case it is possible that the container gets into a corrupted state. 93 | 94 | If this is the case you will have to delete the container: 95 | 96 | ```sh 97 | docker rm -f CONTAINER_ID 98 | ``` 99 | 100 | where `CONTAINER_ID` is the id of the emulator container. 101 | 102 | ## I am not seeing any video in the demo when selecting webrtc 103 | 104 | 1. Click the png button. This will not use webrtc but request individual 105 | screenshots from the emulator. If this works you learn the following: 106 | 107 | - The emulator is running. 108 | - The gRPC endpoint is properly working. 109 | 110 | If the button does not show the emulator then you are possibly running an 111 | older emulator without gRPC support. Make sure you use the latest canary 112 | build. 113 | 114 | 2. I do see video when using the png button. 115 | 116 | - Click the `webrtc` button. Make sure no video is showing. 117 | - Check the JavaScript console log. 118 | 119 | If you only see: `handleJsepMessage: {"start":{}}` then the video bridge is 120 | not running as expected. You could consult the logs for more info: `docker 121 | logs docker_emulator_1 | egrep "pulse:|video:|version:"` 122 | 123 | If you see something along the lines of: 124 | 125 | ```javascript 126 | handleJsepMessage: {"start":{}} 127 | jsep_protocol_driver.js:124 handleJsepMessage: {"sdp":"i... 128 | label:emulator_video_stream\r\n","type":"offer"} 129 | jsep_protocol_driver.js:76 handlePeerConnectionTrack: connecting [object 130 | RTCTrackEvent] 131 | webrtc_view.js:42 Connecting video stream: [object HTMLVideoElement]:0 132 | jsep_protocol_driver.js:124 handleJsepMessage: 133 | {"candidate":"candidate:3808623835 1 udp 2122260223 172.20.0.4 38033 typ 134 | host generation 0 ufrag kyFW network-id 1","sdpMLineIndex":0,"sdpMid":"0"} 135 | jsep_protocol_driver.js:124 handleJsepMessage: 136 | {"candidate":"candidate:2325047 1 udp 1686052607 104.132.0.73 59912 typ 137 | srflx raddr 172.20.0.4 rport 38033 generation 0 ufrag kyFW network-id 138 | 1","sdpMLineIndex":0,"sdpMid":"0"} 139 | webrtc_view.js:50 Automatic playback started! 140 | ``` 141 | 142 | You could be in a situation where 143 | a [TURN](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT) is 144 | needed. This is usually only the case when you are in a restricted network. 145 | You can launch the emulator `$ANDROID_SDK_ROOT/emulator/emulator 146 | -help-turncfg` under linux to learn how to configure turn. You can pass use 147 | `emu_docker create --help` to learn how to pass the `--turncfg` flag to the 148 | emulator. 149 | 150 | 151 | ## Unable to launch docker image 152 | 153 | If you are seeing exceptions during the creation of a docker image directly from python, such as these: 154 | 155 | ```python 156 | Traceback (most recent call last): 157 | File "....python3.6/site-packages/urllib3/connectionpool.py", line 600, in urlopen 158 | chunked=chunked) 159 | File ".../python3.6/site-packages/urllib3/connectionpool.py", line 354, in _make_request 160 | conn.request(method, url, **httplib_request_kw) 161 | File "/usr/lib/python3.6/http/client.py", line 1239, in request 162 | self._send_request(method, url, body, headers, encode_chunked) 163 | File "/usr/lib/python3.6/http/client.py", line 1285, in _send_request 164 | self.endheaders(body, encode_chunked=encode_chunked) 165 | File "/usr/lib/python3.6/http/client.py", line 1234, in endheaders 166 | self._send_output(message_body, encode_chunked=encode_chunked) 167 | File "/usr/lib/python3.6/http/client.py", line 1065, in _send_output 168 | self.send(chunk) 169 | File "/usr/lib/python3.6/http/client.py", line 986, in send 170 | self.sock.sendall(data) 171 | ConnectionResetError: [Errno 104] Connection reset by peer 172 | ``` 173 | 174 | One of the possibilities is that you have not properly configured your credential helpers. Launch withe `-v` flag to 175 | see how docker tries to authenticate to your local service. 176 | 177 | ## Credential errors with docker-compose 178 | 179 | We have seen errors when running docker-compose from a virtual environment. 180 | 181 | The easiest solution is not to use docker-compose from a virtual environment. 182 | 183 | ## Trouble compiling protoc plugins with Homebrew 184 | 185 | It is possible that `pkgconfig` is not able to find the proper location of your protobuf libraries. 186 | This can happen if you are using homebrew with uncommon install location. The easiest way around this 187 | is to explicitly set the pkg-config directory to point to your libprotobuf description. For example: 188 | 189 | ```sh 190 | export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$(find $(brew --prefix) -name 'pkgconfig' -print | grep protobuf) 191 | ``` 192 | 193 | Now you can install the protoc-plugin as follows: 194 | 195 | ```sh 196 | cd js/protoc-plugin 197 | make 198 | sudo make install 199 | ``` 200 | -------------------------------------------------------------------------------- /emu/containers/docker_container.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import logging 3 | import os 4 | import re 5 | import shutil 6 | from pathlib import Path 7 | from typing import Optional 8 | 9 | import docker 10 | from docker.models.images import Image 11 | 12 | from emu.containers.progress_tracker import ProgressTracker 13 | 14 | 15 | class DockerContainer: 16 | """A Docker Device is capable of creating and launching docker images. 17 | 18 | In order to successfully create and launch a docker image you must either 19 | run this as root, or have enabled sudoless docker. 20 | """ 21 | 22 | TAG_REGEX: re.Pattern = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9._-]*:?[a-zA-Z0-9._-]*") 23 | 24 | # Update this once arm is really an option. 25 | DEFAULT_PLATFORM = "linux/amd64" 26 | 27 | def __init__(self, repo: Optional[str] = None): 28 | if repo and repo[-1] != "/": 29 | repo += "/" 30 | self.repo: Optional[str] = repo 31 | 32 | def get_client(self) -> docker.DockerClient: 33 | return docker.from_env() 34 | 35 | def get_api_client(self) -> docker.APIClient: 36 | try: 37 | api_client = docker.APIClient() 38 | logging.info(api_client.version()) 39 | return api_client 40 | except Exception as _err: 41 | logging.exception( 42 | "Failed to create default client, trying domain socket.", exc_info=True 43 | ) 44 | 45 | api_client = docker.APIClient(base_url="unix://var/run/docker.sock") 46 | logging.info(api_client.version()) 47 | return api_client 48 | 49 | def push(self) -> None: 50 | image: str = self.full_name() 51 | print( 52 | f"Pushing docker image: {self.full_name()}.. be patient this can take a while!" 53 | ) 54 | 55 | tracker: ProgressTracker = ProgressTracker() 56 | try: 57 | client: docker.DockerClient = docker.from_env() 58 | result = client.images.push(image, "latest", stream=True, decode=True) 59 | for entry in result: 60 | tracker.update(entry) 61 | self.docker_image().tag(f"{self.repo}{self.image_name()}:latest") 62 | except docker.errors.APIError as err: 63 | logging.error("Failed to push image due to %s", err, exc_info=True) 64 | logging.warning("You can manually push the image as follows:") 65 | logging.warning("docker push %s", image) 66 | 67 | def launch(self, port_map) -> Image: 68 | """Launches the container with the given sha, publishing abd on port, and gRPC on port 8554 69 | 70 | Returns the container. 71 | """ 72 | image: Image = self.docker_image() 73 | client: docker.DockerClient = docker.from_env() 74 | try: 75 | container = client.containers.run( 76 | image=image.id, 77 | privileged=True, 78 | publish_all_ports=True, 79 | detach=True, 80 | ports=port_map, 81 | ) 82 | print(f"Launched {container.name} (id:{container.id})") 83 | print(f"docker logs -f {container.name}") 84 | print(f"docker stop {container.name}") 85 | return container 86 | except Exception as err: 87 | logging.exception("Unable to run the %s due to %s", image.id, err) 88 | print("Unable to start the container, try running it as:") 89 | print(f"./run.sh {image.id}") 90 | 91 | def create_container(self, dest: Path) -> str: 92 | """Creates the docker container, returning the sha of the container, or None in case of failure.""" 93 | identity = None 94 | image_tag = self.full_name() 95 | print(f"docker build {dest} -t {image_tag}") 96 | try: 97 | api_client = self.get_api_client() 98 | logging.info( 99 | "build(path=%s, tag=%s, rm=True, decode=True)", dest, image_tag 100 | ) 101 | result = api_client.build( 102 | path=str(dest.absolute()), tag=image_tag, rm=True, decode=True, 103 | platform=DockerContainer.DEFAULT_PLATFORM 104 | ) 105 | for entry in result: 106 | if "stream" in entry and entry["stream"].strip(): 107 | logging.info(entry["stream"]) 108 | if "aux" in entry and "ID" in entry["aux"]: 109 | identity = entry["aux"]["ID"] 110 | if "error" in entry: 111 | logging.error(entry["error"]) 112 | client = docker.from_env() 113 | image = client.images.get(identity) 114 | image.tag(self.repo + self.image_name(), "latest") 115 | except docker.errors.APIError as err: 116 | logging.error("Failed to create container due to %s.", err, exc_info=True) 117 | logging.warning("You can manually create the container as follows:") 118 | logging.warning("docker build -t %s %s", image_tag, dest) 119 | 120 | return identity 121 | 122 | def clean(self, dest: Path) -> None: 123 | if dest.exists(): 124 | shutil.rmtree(dest) 125 | 126 | dest.mkdir(parents=True) 127 | 128 | def pull(self) -> Image: 129 | """Tries to retrieve the given image and tag. 130 | 131 | Return True if succeeded, False when failed. 132 | """ 133 | client = self.get_api_client() 134 | try: 135 | tracker = ProgressTracker() 136 | result = client.pull(self.repo + self.image_name(), self.docker_tag()) 137 | for entry in result: 138 | tracker.update(entry) 139 | except docker.errors.APIError as err: 140 | logging.debug( 141 | "Unable to pull image %s%s:%s due to %s", 142 | self.repo, 143 | self.image_name(), 144 | self.docker_tag(), 145 | err, 146 | ) 147 | return None 148 | 149 | # We obtained the image, so it should exist. 150 | return self.docker_image() 151 | 152 | def full_name(self) -> str: 153 | """Gets the complete name of this image, this can be used in the build step. 154 | Generally there are two modes: 155 | 156 | - It is available in a remote repository 157 | - It is available locally 158 | """ 159 | local = self.docker_image() 160 | 161 | # Available locally, get the first tag 162 | if local: 163 | return local.tags[0] 164 | 165 | return "" 166 | 167 | def latest_name(self): 168 | if self.repo: 169 | return f"{self.repo}{self.image_name()}:{self.docker_tag()}" 170 | return (self.image_name(), "latest") 171 | 172 | def create_cloud_build_step(self, dest: Path): 173 | return { 174 | "name": "gcr.io/cloud-builders/docker", 175 | "args": [ 176 | "build", 177 | "-t", 178 | self.full_name(), 179 | "-t", 180 | self.latest_name(), 181 | os.path.basename(dest), 182 | ], 183 | } 184 | 185 | def docker_image(self) -> Image: 186 | """The docker local docker image if any 187 | 188 | Returns: 189 | {docker.models.images.Image}: A docker image object, or None. 190 | """ 191 | client = self.get_client() 192 | for img in client.images.list(): 193 | for tag in img.tags: 194 | if self.image_name() in tag: 195 | return img 196 | return None 197 | 198 | def available(self): 199 | """True if this container image is locally available.""" 200 | if self.docker_image(): 201 | logging.info("%s is avaliable", self.docker_image().tags) 202 | return True 203 | return False 204 | 205 | def build(self, dest: Path): 206 | logging.info("Building %s in %s", self, dest) 207 | self.write(Path(dest)) 208 | return self.create_container(Path(dest)) 209 | 210 | def can_pull(self): 211 | """True if this container image can be pulled from a registry.""" 212 | return self.pull() is not None 213 | 214 | @abc.abstractmethod 215 | def write(self, destination: Path): 216 | """Method responsible for writing the Dockerfile and all necessary files to build a container. 217 | 218 | Args: 219 | destination ({string}): A path to a directory where all the container files should reside. 220 | 221 | Raises: 222 | NotImplementedError: [description] 223 | """ 224 | raise NotImplementedError() 225 | 226 | @abc.abstractmethod 227 | def image_name(self): 228 | """The image name without the tag used to uniquely identify this image. 229 | 230 | Raises: 231 | NotImplementedError: [description] 232 | """ 233 | raise NotImplementedError() 234 | 235 | @abc.abstractmethod 236 | def docker_tag(self): 237 | raise NotImplementedError() 238 | 239 | @abc.abstractmethod 240 | def depends_on(self): 241 | """Name of the system image this container is build on.""" 242 | raise NotImplementedError() 243 | 244 | def __str__(self): 245 | return self.image_name() + ":" + self.docker_tag() 246 | -------------------------------------------------------------------------------- /js/turn/README.MD: -------------------------------------------------------------------------------- 1 | # Using a TURN server 2 | 3 | This document describes how you can use a turn server and includes a simple 4 | example of how you could deploy your own. You would need a turn server if you 5 | are running the emulator in network that is not publicly accessible. 6 | 7 | To enable turn you must have the following: 8 | 9 | - A publicly accessible turn server. 10 | - A mechanism to create a JSON turn configuration snippet the emulator can give 11 | to the browser clients. 12 | 13 | Keep in mind that a savvy user could extract this snippet from the browser. Do not hand out permanent valid configurations 14 | if you do not trust your end users. A malicious user could use your TURN server for their own relay. 15 | 16 | ## Quick start turn server. 17 | 18 | In this example we will use [coturn](https://github.com/coturn/coturn) as our turn server that uses a fixed set of credentials. You can use this configuration if you trust your users, or if you quickly want to test something. 19 | 20 | 1. Launch the turn server that is publicly accessible (say my.turn.org): 21 | 22 | In the example below we are using an _insecure_ turn server. You should at least 23 | provide a configuration file with passwords if you are going to deploy this. 24 | 25 | ```sh 26 | export TURN_SERVER="localhost" # This should be your real host name! 27 | turnserver -v -n --log-file=stdout -r $TURN_SERVER 28 | ``` 29 | 30 | You can find details on using long term credentials in turn [here](https://github.com/coturn/coturn/wiki/README). 31 | 32 | 2. Create the json snippet, that provides access: 33 | 34 | ```js 35 | { 36 | "iceServers": [ 37 | { 38 | "urls": "turn:$TURN_SERVER", 39 | "username": "webrtc", 40 | "credential": "turnpassword", 41 | }, 42 | ]; 43 | } 44 | ``` 45 | 46 | Note that the username and password depend on how you configured your turn server and 47 | are not needed if you are running an insecure server. 48 | 49 | 3. Launch the emulator container with the turncfg flag with the snippet as a single line: 50 | 51 | ```sh 52 | export SNIPPET="{\"iceServers\":[{\"urls\":\"turn:$TURN_SERVER\",\"username\":\"webrtc\",\"credential\":\"turnpassword\"}]}" 53 | docker run \ 54 | -e ADBKEY="$(cat ~/.android/adbkey)" \ 55 | -e TURN="printf $SNIPPET" \ 56 | --device /dev/kvm \ 57 | --publish 8554:8554/tcp \ 58 | --publish 5555:5555/tcp ${CONTAINER_ID} 59 | ``` 60 | 61 | If you wish to confirm that this is working properly you can check the docker logs for a line like this: 62 | 63 | ```sh 64 | docker logs my_container_id | grep Switchboard.cpp 65 | video: (Switchboard.cpp:264): Sending {"msg":"{\"start\":{\"iceServers\":[{\"credential\":\"turnpassword\",\"urls\":\"turn:\",\"username\":\"webrtc\"}]}}","topic":"c6965c12-8b72-45c3-bc7d-f8143488382a"} 66 | ``` 67 | 68 | ## Quick start using secure turn with temporary credentials 69 | 70 | In this example we will use [coturn](https://github.com/coturn/coturn) as our turn server, and we will use a simple python rest api to provide us with a secure configuration. The emulator will call the rest api which in turn will create a valid configuration that the turn server accepts. 71 | 72 | For this you will need: 73 | 74 | - A public coTURN server. 75 | - A shared secret between the coTURN server and the Python REST api 76 | - A shared secret between the Python REST api and the emulator. 77 | - [jq](https://stedolan.github.io/jq/), used to quickly test your config 78 | - [coturn](https://github.com/coturn/coturn), you will use the utilities to test and the server. 79 | 80 | 1. Launch the Python REST api server. This does not have to be public, but must 81 | be accessible by the emulator (say turn_key.provider.org) 82 | 83 | ```sh 84 | export API_KEY="a_secret_the_emulator_needs" 85 | export SECRET="a_secret_that_the_turn_server_needs" 86 | python turn.py --turn_secret $SECRET --api_key $API_KEY --port 8080 87 | ``` 88 | 89 | Make sure that the api server works and is accessible, for example 90 | 91 | ```sh 92 | export API_SERVER=http://turn_key.provider.org 93 | curl -s $API_SERVER/turn/localhost?apiKey=$API_KEY 94 | ``` 95 | 96 | Should return something like this: 97 | 98 | ```js 99 | {"iceServers":[{"credential":"dFSc+Cg119PBHBx+qodUPI/19ic=","urls":["turn:localhost"],"username":"1598484959:someone"}]} 100 | ``` 101 | 102 | 2. Launch the turn server that is publicly accessible (say my.turn.org): 103 | 104 | ```sh 105 | export TURN_SERVER="localhost" # This should be your real host name! 106 | turnserver -v -n --log-file=stdout \ 107 | --use-auth-secret --static-auth-secret=$SECRET -r $TURN_SERVER 108 | ``` 109 | 110 | Make sure your turn server is properly configured by making sure 111 | you can connect with the generated JSON config from the previous step: 112 | 113 | Using [jq](https://stedolan.github.io/jq/) we can construct our test: 114 | 115 | ```sh 116 | curl -s "http://localhost:8123/turn/localhost?apiKey=$API_KEY" | \ 117 | jq '.iceServers[0] | "turnutils_uclient -w \(.credential) -u \(.username) $TURN_SERVER"' 118 | ``` 119 | 120 | Now copy paste the result and execute the command, if all went well you should see results. 121 | For example: 122 | 123 | ```sh 124 | $ turnutils_uclient -w GH7ON8EceVvLtr96vSIB6unYBRM= -u 1598489324:someone localhost 125 | 126 | 0: Total connect time is 0 127 | 1: start_mclient: msz=2, tot_send_msgs=0, tot_recv_msgs=0, tot_send_bytes ~ 0, tot_recv_bytes ~ 0 128 | 2: start_mclient: msz=2, tot_send_msgs=4, tot_recv_msgs=0, tot_send_bytes ~ 400, tot_recv_bytes ~ 0 129 | 3: start_mclient: msz=2, tot_send_msgs=5, tot_recv_msgs=0, tot_send_bytes ~ 500, tot_recv_bytes ~ 0 130 | 4: start_mclient: msz=2, tot_send_msgs=5, tot_recv_msgs=0, tot_send_bytes ~ 500, tot_recv_bytes ~ 0 131 | 5: start_mclient: msz=2, tot_send_msgs=5, tot_recv_msgs=0, tot_send_bytes ~ 500, tot_recv_bytes ~ 0 132 | ``` 133 | 134 | Congratulations! You have a secure working turn server. 135 | 136 | 3. Launch the emulator container with the turncfg flag: 137 | 138 | ```sh 139 | docker run \ 140 | -e ADBKEY="$(cat ~/.android/adbkey)" \ 141 | -e TURN="curl -s $TURN_API/turn/$TURN_SERVER\?apiKey=$API_KEY" \ 142 | --device /dev/kvm \ 143 | --publish 8554:8554/tcp \ 144 | --publish 5555:5555/tcp ${CONTAINER_ID} 145 | ``` 146 | 147 | You should now be able to launch the web application which should make use of your turn service. 148 | If all went well you should see loglines such as these: 149 | 150 | ``` 151 | emulator: INFO: RtcService.cpp:98: RtcPacket: id { guid: "6da5cc46-7afc-4409-a77c-315dfe418f83" } message: "{\"start\":{\"iceServers\":[{\"credential\":\"mzddNrUmJcvp9Xg4q1ttzasd8Qk=\",\"urls\":[\"turn:localhost\"],\"username\":\"1598489788:someone\"}]}}" 152 | ``` 153 | 154 | The emulator is informing your client to use your turn server 155 | 156 | ## Custom configurations 157 | 158 | The emulator has support for turn if you have a command that does the following: 159 | 160 | - Produce a result on stdout. 161 | - Produce a result within 1000 ms. 162 | - Produce a valid [JSON RTCConfiguration object](https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration). 163 | - That contains at least an "iceServers" array. 164 | - The exit value should be 0 on success 165 | 166 | This command can be passed in with the -turncfg parameter to the emulator. For example: 167 | 168 | ```sh 169 | emulator -grpc 8554 -turncfg 'printf {"iceServers":[{"urls":["stun:stun.l.google.com:19302"]}]}' 170 | ``` 171 | 172 | This will use the standard stun server that Google provides. In general we support two approaches out of the box: 173 | 174 | - A static configuration: You can use `printf` as shown above to produce the static snippet. 175 | - A dynamic configuration: The emulator container ships with `curl`, which can be used to obtain a snippet. 176 | Make sure to pass in the `-s` flag to keep curl silent. 177 | 178 | Both approaches can meet the requirements mentioned above. 179 | 180 | ### Enable turn in containers. 181 | 182 | There are two ways in which we can embed the turn configuration inside the emulator: 183 | 184 | - **During run time:** Each time when we launch the emulator we add a flag. 185 | 186 | You can pass in additional parameters to the emulator by setting 187 | `TURN` environment variable in the container you wish to launch. 188 | The environment variable should contain the command you wish to execute to 189 | obtain the turn configuration, for example you could use curl to obtain a 190 | a snippet: 191 | 192 | ```sh 193 | docker run \ 194 | -e ADBKEY="$(cat ~/.android/adbkey)" \ 195 | -e TURN="curl -s -X POST https://networktraversal.googleapis.com/v1alpha/iceconfig?key=mykey" \ 196 | --device /dev/kvm \ 197 | --publish 8554:8554/tcp \ 198 | --publish 5555:5555/tcp ${CONTAINER_ID} 199 | ``` 200 | 201 | - **During build time:** Embed the additional flag in the container itself. Everyone who launches your created container will use this flag unless it is manually overriden! 202 | 203 | You can create the docker container with the `--extra` flag to pass in the turn configuration. For example to use a static configuration: 204 | 205 | ```sh 206 | emu-docker create canary \ 207 | "R" \ 208 | --extra \ 209 | '-turncfg '\\\''printf {\"iceServers\":[{\"urls\":\"turn:\",\"username\":\"webrtc\",\"credential\":\"turnpassword\"}]}'\\\'' ' 210 | ``` 211 | 212 | Note the `'\\\''` to escape a single `'` and `\"` to escape `"`, as we want pass the following flag to emulator: 213 | 214 | ```sh 215 | -turncfg 'printf {"iceServers":[{"urls":"turn:","username":"webrtc","credential":"turnpassword"}]}' 216 | ``` 217 | 218 | when launching the emulator. 219 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------