├── tests ├── __init__.py └── test_routes.py ├── webapp ├── __init__.py ├── views.py ├── macaroons.py └── app.py ├── .prettierignore ├── charm ├── requirements.txt ├── .gitignore ├── charmcraft.yaml └── src │ └── charm.py ├── templates ├── privacy.html ├── login │ └── index.html ├── sitemap │ ├── sitemap-index.xml │ └── sitemap-links.xml ├── 401.html ├── 404.html ├── thank-you.html ├── 500.html ├── partial │ ├── _navigation.html │ └── _footer.html ├── base_layout.html ├── modals │ └── contact-us.html ├── contact-us.html ├── terms.html └── includes │ └── _country-select.html ├── static ├── js │ ├── modals │ │ ├── index.js │ │ ├── intlTelInput.js │ │ └── dynamic-forms.js │ ├── detect-device.js │ ├── anbox-stream-docs.md │ ├── contextualMenu.js │ ├── docs-side-nav.js │ └── anbox-stream-sdk.js ├── files │ └── robots.txt ├── sass │ ├── _settings.scss │ ├── _intl-tel-input.scss │ ├── _pattern_footer.scss │ ├── _pattern_navigation.scss │ ├── _pattern_matrix.scss │ ├── _pattern_heading-icon.scss │ ├── _pattern_stream.scss │ ├── styles.scss │ └── _pattern_strip.scss └── logo.svg ├── .env ├── renovate.json ├── requirements.txt ├── entrypoint ├── scripts ├── parseDocsLinkcheckerOutput └── linkcheckerrc ├── .dockerignore ├── .djlintrc ├── app.py ├── .github ├── pull_request_template.md └── workflows │ ├── docs-links.yaml │ ├── pr.yaml │ └── deploy.yaml ├── rockcraft.yaml ├── konf └── site.yaml ├── .gitignore ├── README.md ├── Dockerfile ├── package.json ├── .sass-lint.yml ├── run └── permanent-redirects.yaml /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /charm/requirements.txt: -------------------------------------------------------------------------------- 1 | ops ~= 2.17 2 | paas-charm>=1.0,<2 3 | -------------------------------------------------------------------------------- /templates/privacy.html: -------------------------------------------------------------------------------- 1 |

Privacy policy

2 | 3 | 4 | -------------------------------------------------------------------------------- /static/js/modals/index.js: -------------------------------------------------------------------------------- 1 | import "./dynamic-forms.js"; 2 | import "./intlTelInput.js"; 3 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PORT=8043 2 | FLASK_DEBUG=true 3 | SECRET_KEY=secret_key 4 | FLASK_SECRET_KEY=secret_key 5 | DEVEL=true 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>canonical-web-and-design/renovate-websites" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /charm/.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | build/ 3 | *.charm 4 | .tox/ 5 | .coverage 6 | __pycache__/ 7 | *.py[cod] 8 | .idea 9 | .vscode/ 10 | -------------------------------------------------------------------------------- /static/files/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: /static/js/modules/global-nav/global-nav.js 3 | Disallow: /docs/search 4 | Disallow: /docs/search* 5 | -------------------------------------------------------------------------------- /templates/login/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_layout.html' %} {% block title %} This is the login/index page {% endblock %} 2 | {% block content %} 3 |

{{greeting}}

4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | canonicalwebteam.flask-base==2.3.0 2 | Flask-OpenID==1.3.1 3 | requests==2.32.3 4 | responses==0.25.7 5 | pymacaroons==0.13.0 6 | canonicalwebteam.image-template==1.5.0 7 | maxminddb-geolite2==2018.703 8 | -------------------------------------------------------------------------------- /templates/sitemap/sitemap-index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://anbox-cloud.io/sitemap-links.xml 5 | 6 | 7 | -------------------------------------------------------------------------------- /static/sass/_settings.scss: -------------------------------------------------------------------------------- 1 | $ubuntu-orange: #e95420; 2 | $font-use-subset-latin: true; 3 | $color-accent: $ubuntu-orange; 4 | $color-suru-start: #2c001e; 5 | $color-suru-middle: #772953; 6 | $color-suru-end: #e95420; 7 | $breakpoint-navigation-threshold: 620px; // keep this in sync with global nav config in base_layout.html 8 | $table-layout-fixed: false; 9 | -------------------------------------------------------------------------------- /entrypoint: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -e 4 | 5 | RUN_COMMAND="talisker.gunicorn.gevent webapp.app:app --bind $1 --worker-class gevent --name talisker-`hostname`" 6 | 7 | if [ "${FLASK_DEBUG}" = true ] || [ "${FLASK_DEBUG}" = 1 ]; then 8 | RUN_COMMAND="${RUN_COMMAND} --reload --log-level debug --timeout 9999" 9 | fi 10 | 11 | ${RUN_COMMAND} 12 | -------------------------------------------------------------------------------- /scripts/parseDocsLinkcheckerOutput: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! ls $HOME/.linkchecker/linkchecker-out.txt; then 4 | echo "No linkchecker output found" 5 | exit 1 6 | fi 7 | 8 | if grep -q "Error: 4" $HOME/.linkchecker/linkchecker-out.txt; then 9 | cat $HOME/.linkchecker/linkchecker-out.txt 10 | exit 1 11 | else 12 | echo "No 400 errors were detected" 13 | exit 0 14 | fi 15 | -------------------------------------------------------------------------------- /scripts/linkcheckerrc: -------------------------------------------------------------------------------- 1 | [checking] 2 | maxrequestspersecond=5 3 | 4 | [filtering] 5 | nofollow=!https:\/\/anbox-cloud\.io\/docs\/ 6 | checkextern=1 7 | ignore= 8 | https://res\.cloudinary\.com 9 | .*&start= 10 | /q_auto 11 | /fl_sanitize 12 | /w_ 13 | /h_ 14 | https://assets\.ubuntu\.com 15 | https://bugs.launchpad.net/indore-extern/* 16 | 17 | [output] 18 | status=0 19 | warnings=0 20 | -------------------------------------------------------------------------------- /charm/charmcraft.yaml: -------------------------------------------------------------------------------- 1 | name: anbox-cloud-io 2 | 3 | type: charm 4 | 5 | bases: 6 | - build-on: 7 | - name: ubuntu 8 | channel: "22.04" 9 | run-on: 10 | - name: ubuntu 11 | channel: "22.04" 12 | 13 | summary: This is the charm for the anbox-cloud.io website. 14 | 15 | description: This is the charm for the anbox-cloud.io website. 16 | 17 | extensions: 18 | - flask-framework 19 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Don't include caches, logs and documentation 2 | *.log 3 | .circleci 4 | .codecov 5 | .git 6 | .github 7 | .gitignore 8 | .vscode 9 | cache.sqlite 10 | README.md 11 | HACKING.md 12 | 13 | # The run script isn't needed 14 | run 15 | .docker-project 16 | 17 | # Environemnt 18 | .env 19 | .env.local 20 | Dockerfile 21 | LICENSE 22 | renovate.json 23 | tests 24 | 25 | # Node is only needed for building, not running 26 | node_modules 27 | -------------------------------------------------------------------------------- /static/js/detect-device.js: -------------------------------------------------------------------------------- 1 | export default function mobileDevice() { 2 | if (navigator.userAgent.match(/Android/i) 3 | || navigator.userAgent.match(/webOS/i) 4 | || navigator.userAgent.match(/iPhone/i) 5 | || navigator.userAgent.match(/iPad/i) 6 | || navigator.userAgent.match(/iPod/i) 7 | || navigator.userAgent.match(/BlackBerry/i) 8 | || navigator.userAgent.match(/Windows Phone/i)) 9 | return true 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /.djlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "blank_line_after_tag": "load,extends", 3 | "blank_line_before_tag": "load,extends,block, macro", 4 | "close_void_tags": true, 5 | "ignore": "J018,H006,H021,H023,H029,T002,T003,T028,D018", 6 | "include": "H017", 7 | "indent": "2", 8 | "max_blank_lines": 1, 9 | "max_line_length": "120", 10 | "profile": "jinja", 11 | "format_css": true, 12 | "format_js": true, 13 | "css": { 14 | "indent_size": 2 15 | }, 16 | "js": { 17 | "indent_size": 2 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /static/sass/_intl-tel-input.scss: -------------------------------------------------------------------------------- 1 | // import dial-code dropdown 2 | @import "intl-tel-input/build/css/intlTelInput"; 3 | 4 | // Overide the path to flags for dial codes 5 | .iti__flag { 6 | background-image: url("//assets.ubuntu.com/v1/21572c97-flags.png"); 7 | } 8 | 9 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { 10 | .iti__flag { 11 | background-image: url("//assets.ubuntu.com/v1/d6f84371-flags%402x.png"); 12 | } 13 | } 14 | 15 | .iti { 16 | width: 100%; 17 | } 18 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # This file serves as an entry point for the rock image. It is required by the PaaS app charmer. 2 | # The flask application must be defined in this file under the variable name `app`. 3 | # See - https://documentation.ubuntu.com/rockcraft/en/latest/reference/extensions/flask-framework/ 4 | import os 5 | 6 | # canonicalwebteam.flask-base requires SECRET_KEY to be set, this must be done before importing the app 7 | os.environ["SECRET_KEY"] = os.environ["FLASK_SECRET_KEY"] 8 | 9 | from webapp.app import app 10 | -------------------------------------------------------------------------------- /static/sass/_pattern_footer.scss: -------------------------------------------------------------------------------- 1 | @mixin canonical-p-footer { 2 | .p-footer { 3 | @extend %vf-strip; 4 | 5 | background: $colors--dark-theme--background-alt; 6 | color: $colors--dark-theme--text-default; 7 | 8 | .p-list__item--condensed { 9 | @extend %vf-list-item; 10 | 11 | padding-bottom: 0; 12 | padding-top: 0; 13 | } 14 | 15 | a { 16 | color: $color-link-dark; 17 | 18 | &:visited { 19 | color: $color-link-visited-dark; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /static/sass/_pattern_navigation.scss: -------------------------------------------------------------------------------- 1 | @mixin anbox-p-navigation { 2 | .p-navigation { 3 | &__row { 4 | &-banner { 5 | padding: 0 ($sph--small + $sph--large); 6 | 7 | @media only screen and (max-width: $breakpoint-small) { 8 | padding-left: 1rem; 9 | } 10 | } 11 | 12 | &-logo { 13 | font-weight: 400; 14 | margin-right: 0; 15 | } 16 | 17 | &-toggle { 18 | &--open, 19 | &--close { 20 | font-weight: 400; 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Done 2 | 3 | [List of work items including drive-bys] 4 | 5 | ## QA 6 | 7 | - Check out this feature branch 8 | - Run the site using the command `./run serve` 9 | - View the site locally in your web browser at: http://0.0.0.0:8043/ 10 | - Run through the following [QA steps](https://webteam.canonical.com/practices/qa-steps) 11 | - [List additional steps to QA the new features or prove the bug has been resolved] 12 | 13 | 14 | ## Issue / Card 15 | 16 | Fixes # 17 | 18 | ## Screenshots 19 | 20 | [if relevant, include a screenshot] 21 | -------------------------------------------------------------------------------- /static/sass/_pattern_matrix.scss: -------------------------------------------------------------------------------- 1 | @mixin anbox-p-matrix { 2 | .p-matrix--two-col { 3 | .p-matrix__item { 4 | @media (min-width: $breakpoint-small) { 5 | width: 50%; 6 | 7 | &:nth-child(2n + 1) { 8 | border-right: 1px solid $color-mid-light; 9 | padding-left: 0; 10 | } 11 | 12 | &:nth-child(2n) { 13 | border-right: 0; 14 | padding-left: $spv--large; 15 | } 16 | 17 | &:nth-child(3) { 18 | border-top: 1px solid $color-mid-light; 19 | } 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /static/sass/_pattern_heading-icon.scss: -------------------------------------------------------------------------------- 1 | @mixin anbox-p-heading-icon { 2 | .p-heading-icon--muted { 3 | .p-heading-icon__header { 4 | margin-bottom: map-get($sp-after, small) - map-get($nudges, nudge--small); 5 | } 6 | 7 | .p-heading-icon__title { 8 | @extend %muted-heading; 9 | 10 | align-items: center; 11 | display: flex; 12 | line-height: 1.875rem; 13 | margin-bottom: 0; 14 | } 15 | } 16 | 17 | .p-heading-icon__img--small { 18 | align-self: center; 19 | height: auto; 20 | margin-bottom: 0; 21 | margin-top: .25rem; 22 | max-width: 2rem; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /templates/sitemap/sitemap-links.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://anbox-cloud.io/ 5 | weekly 6 | 7 | 8 | 9 | https://anbox-cloud.io/contact-us 10 | weekly 11 | 12 | 13 | 14 | https://anbox-cloud.io/terms 15 | weekly 16 | 17 | 18 | 19 | https://anbox-cloud.io/privacy 20 | weekly 21 | 22 | -------------------------------------------------------------------------------- /templates/401.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_layout.html' %} 2 | 3 | {% block title %}401: Not Authorised{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 | Error owl 10 |
11 |
12 |
13 |

401: Unauthorised

14 |

{{ error.capitalize() }}.

15 |
16 |
17 |
18 |
19 | {% endblock content %} 20 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_layout.html' %} 2 | 3 | {% block title %}404: Page not found{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 | Error owl 10 |
11 |
12 |
13 |

404: Page not found

14 |

Sorry, we couldn’t find that page.

15 |
16 |
17 |
18 |
19 | {% endblock content %} 20 | -------------------------------------------------------------------------------- /charm/src/charm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright 2024 Canonical Ltd. 3 | # See LICENSE file for licensing details. 4 | 5 | """Flask Charm entrypoint.""" 6 | 7 | import logging 8 | import typing 9 | 10 | import ops 11 | import paas_charm.flask 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class FlaskCharm(paas_charm.flask.Charm): 17 | """Flask Charm service.""" 18 | 19 | def __init__(self, *args: typing.Any) -> None: 20 | """Initialize the instance. 21 | 22 | Args: 23 | args: passthrough to CharmBase. 24 | """ 25 | super().__init__(*args) 26 | 27 | 28 | if __name__ == "__main__": 29 | ops.main.main(FlaskCharm) 30 | -------------------------------------------------------------------------------- /rockcraft.yaml: -------------------------------------------------------------------------------- 1 | name: anbox-cloud-io 2 | base: ubuntu@22.04 3 | version: "0.1" 4 | summary: Scalable Android in the cloud 5 | description: | 6 | Anbox Cloud lets you stream mobile apps securely, at any scale, to any 7 | device letting you focus on your apps. Run Android in system containers, 8 | not emulators, on AWS, OCI, Azure, GCP or your private cloud with ultra 9 | low streaming latency. 10 | platforms: 11 | amd64: 12 | 13 | extensions: 14 | - flask-framework 15 | 16 | parts: 17 | flask-framework/install-app: 18 | prime: 19 | - flask/app/.env 20 | - flask/app/app.py 21 | - flask/app/webapp 22 | - flask/app/templates 23 | - flask/app/static 24 | - flask/app/permanent-redirects.yaml 25 | -------------------------------------------------------------------------------- /templates/thank-you.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_layout.html' %} 2 | 3 | {% block title %} Thank you | Anbox Cloud {% endblock %} 4 | 5 | {% block description %}Anbox Cloud is the mobile cloud computing platform delivered by Canonical. Run Android in the cloud, at high scale and on any type of hardware. Canonical partners with cloud providers and computing hardware manufacturer to accelerate your time to market and provide long term commercial support.{% endblock %} 6 | 7 | 8 | {% block content %} 9 | 10 |
11 |
12 |
13 |

Thanks for getting in touch

14 |

A member of our team will be in touch within
one working day.

15 |
16 |
17 |
18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /webapp/views.py: -------------------------------------------------------------------------------- 1 | import flask 2 | from geolite2 import geolite2 3 | 4 | ip_reader = geolite2.reader() 5 | 6 | 7 | def get_user_country_by_ip(): 8 | client_ip = flask.request.headers.get( 9 | "X-Real-IP", flask.request.remote_addr 10 | ) 11 | ip_location = ip_reader.get(client_ip) 12 | 13 | try: 14 | country_code = ip_location["country"]["iso_code"] 15 | except KeyError: 16 | # geolite2 can't identify IP address 17 | country_code = None 18 | except Exception: 19 | # Errors not documented in the geolite2 module 20 | country_code = None 21 | 22 | response = flask.jsonify( 23 | { 24 | "client_ip": client_ip, 25 | "country_code": country_code, 26 | } 27 | ) 28 | response.cache_control.private = True 29 | 30 | return response 31 | -------------------------------------------------------------------------------- /konf/site.yaml: -------------------------------------------------------------------------------- 1 | domain: anbox-cloud.io 2 | 3 | image: prod-comms.ps5.docker-registry.canonical.com/anbox-cloud.io 4 | 5 | env: 6 | - name: SENTRY_DSN 7 | value: https://37706e022f2841448dbb094990420522@sentry.is.canonical.com//23 8 | 9 | - name: FLASK_SECRET_KEY 10 | secretKeyRef: 11 | key: anbox-cloud-io 12 | name: secret-keys 13 | 14 | production: 15 | replicas: 5 16 | nginxConfigurationSnippet: | 17 | if ($host != 'anbox-cloud.io' ) { 18 | rewrite ^ https://anbox-cloud.io$request_uri? permanent; 19 | } 20 | more_set_headers "Link: ; rel=preconnect;crossorigin, ; rel=preconnect"; 21 | 22 | staging: 23 | replicas: 3 24 | nginxConfigurationSnippet: | 25 | more_set_headers "X-Robots-Tag: noindex"; 26 | more_set_headers "Link: ; rel=preconnect; crossorigin, ; rel=preconnect"; 27 | -------------------------------------------------------------------------------- /templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_layout.html' %} 2 | 3 | {% block title %}500: Server error{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 | Error owl 10 |
11 |
12 |
13 |

500: Server error

14 |

Something’s gone wrong.

15 | {% if message %}
{{ message }}
{% endif %} 16 |

17 | Try reloading the page. 18 | If the error persists, please note that it may be a known issue. 19 | If not, please file a new issue. 20 |

21 |
22 |
23 |
24 |
25 | {% endblock content %} 26 | -------------------------------------------------------------------------------- /.github/workflows/docs-links.yaml: -------------------------------------------------------------------------------- 1 | name: docs links on anbox-cloud.io/docs 2 | 3 | on: 4 | schedule: 5 | - cron: "0 13 * * *" 6 | 7 | jobs: 8 | check-links: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout main 13 | uses: actions/checkout@v4 14 | 15 | - name: Install linkchecker 16 | run: sudo pip install LinkChecker 17 | 18 | - name: Write linkchecker config file 19 | run: | 20 | mkdir -p ~/.linkchecker 21 | cp scripts/linkcheckerrc ~/.linkchecker/ 22 | 23 | - name: Run linkchecker 24 | continue-on-error: true 25 | run: linkchecker https://anbox-cloud.io/docs > ~/.linkchecker/linkchecker-out.txt 26 | 27 | - name: Parse linkchecker output 28 | run: scripts/parseDocsLinkcheckerOutput 29 | 30 | - name: Send message on failure 31 | if: failure() 32 | run: | 33 | curl -X POST \ 34 | -F "workflow=${GITHUB_WORKFLOW}" \ 35 | -F "repo_name=${GITHUB_REPOSITORY}" \ 36 | -F "action_id=${GITHUB_RUN_ID}" \ 37 | ${{ secrets.BOT_URL }}?room=docs 38 | -------------------------------------------------------------------------------- /static/js/modals/intlTelInput.js: -------------------------------------------------------------------------------- 1 | import intlTelInput from "intl-tel-input"; 2 | 3 | // Setup dial code dropdown options (intlTelInput) 4 | function setupIntlTelInput(phoneInput) { 5 | const utilsScript = "/static/js/utils.js"; 6 | 7 | // remove name from original input so only the hidden input is submitted 8 | const inputName = phoneInput.name; 9 | phoneInput.removeAttribute("name"); 10 | 11 | intlTelInput(phoneInput, { 12 | utilsScript, 13 | separateDialCode: true, 14 | hiddenInput: inputName, 15 | initialCountry: "auto", 16 | geoIpLookup: async function fetchUserIp(success, failure) { 17 | const response = await fetch("/user-country.json"); 18 | if (!response.ok) { 19 | throw new Error(response.status); 20 | } 21 | const JSONObject = await response.json(); 22 | const countryCode = 23 | JSONObject && JSONObject.country_code ? JSONObject.country_code : "gb"; 24 | 25 | success(countryCode); 26 | }, 27 | }); 28 | } 29 | 30 | const targetPhoneInput = document.querySelector("input#phone"); 31 | if (targetPhoneInput) setupIntlTelInput(targetPhoneInput); 32 | 33 | export default setupIntlTelInput; 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # [generated] Bracketed sections updated by Yeoman generator 2 | # generator-canonical-webteam@3.4.3 3 | 4 | # [os] OS & editor files 5 | Desktop.ini 6 | Thumbs.db 7 | ._* 8 | *.DS_Store 9 | *~ 10 | \#*\# 11 | .AppleDouble 12 | .LSOverride 13 | .spelling 14 | .vscode 15 | .idea 16 | 17 | # [cache] Cache and backup 18 | *.bak 19 | *.pyc 20 | *-cache/ 21 | 22 | # [data] Local data 23 | *.sqlite* 24 | *.log 25 | logs/ 26 | pids 27 | *.pid 28 | *.seed 29 | .*-metadata 30 | 31 | # [deps] Local dependencies 32 | .bundle/ 33 | node_modules/ 34 | vendor/ 35 | bower_components/ 36 | vendor/ 37 | # Normally lockfiles would be committed, but we use yarn instead of NPM for locking dependencies 38 | package-lock.json 39 | 40 | # [build] Built files 41 | /build/ 42 | /parts/ 43 | /prime/ 44 | /stage/ 45 | *.egg-info 46 | .snapcraft/ 47 | *.snap 48 | _site/ 49 | *.*.map 50 | *.rock 51 | 52 | # [env] Local environment settings 53 | .docker-project 54 | .*.hash 55 | .envrc 56 | .env.local 57 | env/ 58 | env[23]/ 59 | .coverage 60 | .dotrun.json 61 | .venv 62 | 63 | # [sass] Files generated by Sass 64 | *.css 65 | 66 | node_modules/ 67 | bower_components/ 68 | *.log 69 | 70 | build/ 71 | dist/ 72 | static/js/modules/ 73 | static/js/utils.js 74 | static/js/modals.js 75 | -------------------------------------------------------------------------------- /static/sass/_pattern_stream.scss: -------------------------------------------------------------------------------- 1 | @mixin anbox-p-stream { 2 | .p-stream { 3 | position: relative; 4 | } 5 | 6 | .p-stream__button { 7 | display: block; 8 | position: relative; 9 | } 10 | 11 | .p-stream__status-indicator { 12 | height: 100px; 13 | position: absolute; 14 | right: calc(50% - 50px); 15 | top: calc(50% - 50px); 16 | width: 100px; 17 | } 18 | 19 | .p-stream__player { 20 | position: absolute; 21 | top: 0; 22 | 23 | video { 24 | background-color: $color-x-dark; 25 | margin: 0; 26 | } 27 | 28 | // sass-lint:disable no-vendor-prefixes 29 | // Ensure we always hide the media controls of the player, even in fullscreen. 30 | // See https://css-tricks.com/custom-controls-in-html5-video-full-screen/ for 31 | // more details 32 | video::-webkit-media-controls { 33 | display: none !important; 34 | } 35 | // sass-lint:enable no-vendor-prefixes 36 | } 37 | 38 | .p-stream__instructions { 39 | text-align: left; 40 | 41 | @media (min-width: $breakpoint-small) { 42 | bottom: $spv--medium; 43 | left: $sph--large; 44 | position: absolute; 45 | } 46 | } 47 | 48 | .p-stream__error { 49 | left: $sph--large; 50 | position: absolute; 51 | top: $spv--medium; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /webapp/macaroons.py: -------------------------------------------------------------------------------- 1 | from openid.extension import Extension as OpenIDExtension 2 | 3 | 4 | class MacaroonRequest(OpenIDExtension): 5 | ns_uri = "http://ns.login.ubuntu.com/2016/openid-macaroon" 6 | ns_alias = "macaroon" 7 | 8 | def __init__(self, caveat_id): 9 | self.caveat_id = caveat_id 10 | 11 | def getExtensionArgs(self): 12 | """ 13 | Return the arguments to add to the OpenID request query 14 | """ 15 | 16 | return {"caveat_id": self.caveat_id} 17 | 18 | 19 | class MacaroonResponse(OpenIDExtension): 20 | ns_uri = "http://ns.login.ubuntu.com/2016/openid-macaroon" 21 | ns_alias = "macaroon" 22 | 23 | def getExtensionArgs(self): 24 | """ 25 | Return the arguments to add to the OpenID request query 26 | """ 27 | 28 | return {"discharge": self.discharge} 29 | 30 | def fromSuccessResponse(cls, success_response, signed_only=True): 31 | self = cls() 32 | if signed_only: 33 | args = success_response.getSignedNS(self.ns_uri) 34 | else: 35 | args = success_response.message.getArgs(self.ns_uri) 36 | 37 | if not args: 38 | return None 39 | 40 | self.discharge = args["discharge"] 41 | 42 | return self 43 | 44 | fromSuccessResponse = classmethod(fromSuccessResponse) 45 | -------------------------------------------------------------------------------- /static/sass/styles.scss: -------------------------------------------------------------------------------- 1 | @charset 'UTF-8'; 2 | 3 | // import settings 4 | @import "settings"; 5 | 6 | // import custom mixins 7 | 8 | // import cookie policy 9 | // @import "cookie-policy/build/css/cookie-policy"; 10 | 11 | // import vanilla-framework 12 | @import "vanilla-framework/scss/build"; 13 | 14 | // import cookie policy 15 | @import "@canonical/cookie-policy/build/css/cookie-policy"; 16 | 17 | // import site specific patterns and overrides 18 | @import "pattern_heading-icon"; 19 | @import "pattern_matrix"; 20 | @import "pattern_navigation"; 21 | @import "pattern_strip"; 22 | @import "pattern_stream"; 23 | @import "pattern_footer"; 24 | 25 | @include anbox-p-heading-icon; 26 | @include anbox-p-matrix; 27 | @include anbox-p-navigation; 28 | @include anbox-p-strip; 29 | @include anbox-p-stream; 30 | @include canonical-p-footer; 31 | 32 | // import additional styles 33 | @import "intl-tel-input"; 34 | 35 | .p-pull-quote .p-pull-quote__quote:last-of-type::after { 36 | bottom: auto; 37 | } 38 | 39 | @media (min-width: 975px) { 40 | #contact-modal.p-modal .p-modal__dialog { 41 | max-width: 875px; 42 | min-width: 875px; 43 | } 44 | } 45 | 46 | // TODO: to be removed when properly fixed on Vanilla side 47 | // https://github.com/canonical/vanilla-framework/issues/4898 48 | @media screen and (min-width: $breakpoint-navigation-threshold) { 49 | // align navigation items with grid in docs layout 50 | .l-docs__subgrid .p-navigation__row { 51 | padding-left: .5rem; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anbox Cloud Demo site 2 | 3 | [![CircleCI build status](https://circleci.com/gh/canonical-web-and-design/anbox-cloud.io.svg?style=shield)](https://circleci.com/gh/canonical-web-and-design/anbox-cloud.io) [![Code coverage](https://codecov.io/gh/canonical-web-and-design/anbox-cloud.io/branch/master/graph/badge.svg)](https://codecov.io/gh/canonical-web-and-design/anbox-cloud.io) 4 | 5 | Anbox Cloud is the mobile cloud computing platform for running Android at high scale in any cloud. It is portable across x86 and ARM architectures, with GPU and GPGPU support. Canonical will help you deploy your applications to accelerate your time-to-market. Anbox Cloud comes bundled with a long-term commercial support offering. 6 | 7 | ## Architecture overview 8 | 9 | This website is written with the help of the [flask](http://flask.pocoo.org/) framework. In order to use functionality that multiply our websites here at Canonical, we import the [base-flask-extension](https://github.com/canonical-web-and-design/canonicalwebteam.flask-base) module. 10 | 11 | 12 | ## Development 13 | 14 | - Run `./run` inside the root of the repository and all dependencies will automatically be installed. Afterwards the website will be available at . 15 | - To access the Anbox streaming demo you will need an invitation code. You can access it here `http://localhost:8043/login?next=/demo&invitation_code={invitation_code}` 16 | - If you are part of the webteam, you can find an invitation code in lastPass in Anbox cloud demo 17 | 18 | # Deploy 19 | You can find the deployment config in the deploy folder. 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:experimental 2 | 3 | # Build stage: Install python dependencies 4 | # === 5 | FROM ubuntu:jammy AS python-dependencies 6 | RUN apt-get update && apt-get install --no-install-recommends --yes python3-pip python3-setuptools 7 | ADD requirements.txt /tmp/requirements.txt 8 | RUN pip3 config set global.disable-pip-version-check true 9 | RUN --mount=type=cache,target=/root/.cache/pip pip3 install --user --requirement /tmp/requirements.txt 10 | 11 | 12 | # Build stage: Install yarn dependencies 13 | # === 14 | FROM node:20 AS yarn-dependencies 15 | WORKDIR /srv 16 | ADD package.json . 17 | RUN --mount=type=cache,target=/usr/local/share/.cache/yarn yarn install --production 18 | 19 | # Build stage: Run "yarn run build-js" 20 | # === 21 | FROM yarn-dependencies AS build-js 22 | WORKDIR /srv 23 | ADD static/js static/js 24 | ADD build.js build.js 25 | RUN yarn run build-js 26 | 27 | # Build stage: Run "yarn run build-css" 28 | # === 29 | FROM yarn-dependencies AS build-css 30 | ADD static/sass static/sass 31 | RUN yarn run build-css 32 | 33 | # Build the production image 34 | # === 35 | FROM ubuntu:jammy 36 | 37 | COPY . . 38 | # Install python and import python dependencies 39 | RUN apt-get update && apt-get install --no-install-recommends --yes python3 python3-setuptools python3-lib2to3 python3-pkg-resources ca-certificates libsodium-dev 40 | COPY --from=python-dependencies /root/.local/lib/python3.10/site-packages /root/.local/lib/python3.10/site-packages 41 | COPY --from=python-dependencies /root/.local/bin /root/.local/bin 42 | ENV PATH="/root/.local/bin:${PATH}" 43 | 44 | 45 | # Import code, build assets and mirror list 46 | COPY . . 47 | RUN rm -rf package.json yarn.lock requirements.txt 48 | COPY --from=build-css /srv/static/css static/css 49 | COPY --from=build-js /srv/static/js static/js 50 | 51 | # Set revision ID 52 | ARG BUILD_ID 53 | ENV TALISKER_REVISION_ID "${BUILD_ID}" 54 | 55 | # Setup commands to run server 56 | ENTRYPOINT ["./entrypoint"] 57 | CMD ["0.0.0.0:80"] 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Canonical webteam", 3 | "license": "LGPL-3.0-or-later", 4 | "scripts": { 5 | "start": "yarn run build && concurrently --kill-others --raw 'yarn run watch' 'yarn run serve'", 6 | "clean": "rm -rf node_modules yarn-error.log css static/css *.log *.sqlite _site/ build/ .jekyll-metadata .bundle", 7 | "watch": "watch -p 'static/sass/**/*.scss' -c 'yarn run build'", 8 | "build-css": "sass static/sass/styles.scss static/css/styles.css --load-path=node_modules --style=compressed && postcss --map false --use autoprefixer --replace 'static/css/**/*.css'", 9 | "format-python": "black --line-length 79 webapp", 10 | "build-js": "node build.js && yarn run build-global-nav && yarn run build-cookie-policy && yarn run build-intl-tel-input-utils", 11 | "build-global-nav": "mkdir -p static/js/modules/global-nav && cp node_modules/@canonical/global-nav/dist/global-nav.js static/js/modules/global-nav", 12 | "build-cookie-policy": "mkdir -p static/js/modules/cookie-policy && cp node_modules/@canonical/cookie-policy/build/js/cookie-policy.js static/js/modules/cookie-policy", 13 | "build-intl-tel-input-utils": "cp node_modules/intl-tel-input/build/js/utils.js static/js", 14 | "build": "yarn run build-css && yarn run build-js", 15 | "lint-python": "flake8 webapp tests && black --check --line-length 79 webapp tests", 16 | "lint-scss": "sass-lint static/**/*.scss --verbose --no-exit", 17 | "serve": "./entrypoint 0.0.0.0:${PORT}", 18 | "test": "yarn run lint-scss && yarn run lint-python && yarn run test-python", 19 | "test-python": "python3 -m unittest discover tests" 20 | }, 21 | "dependencies": { 22 | "@canonical/cookie-policy": "3.6.5", 23 | "@canonical/global-nav": "3.6.4", 24 | "autoprefixer": "10.4.21", 25 | "esbuild": "0.25.1", 26 | "intl-tel-input": "17.0.21", 27 | "postcss": "8.5.3", 28 | "postcss-cli": "11.0.1", 29 | "sass": "1.86.0", 30 | "vanilla-framework": "4.21.1" 31 | }, 32 | "devDependencies": { 33 | "concurrently": "8.2.2", 34 | "sass-lint": "1.13.1", 35 | "watch-cli": "0.2.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/test_routes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from webapp.app import app 3 | 4 | 5 | class TestRoutes(unittest.TestCase): 6 | def setUp(self): 7 | """ 8 | Set up Flask app for testing 9 | """ 10 | app.testing = True 11 | self.client = app.test_client() 12 | 13 | def test_homepage(self): 14 | """ 15 | When given the index URL, 16 | we should return a 200 status code 17 | """ 18 | 19 | self.assertEqual(self.client.get("/").status_code, 301) 20 | 21 | def test_thank_you(self): 22 | """ 23 | When given the index URL, 24 | we should return a 200 status code 25 | """ 26 | 27 | self.assertEqual(self.client.get("/thank-you").status_code, 301) 28 | 29 | def test_contact_us(self): 30 | """ 31 | When given the index URL, 32 | we should return a 200 status code 33 | """ 34 | 35 | self.assertEqual(self.client.get("/contact-us").status_code, 301) 36 | 37 | def test_not_found(self): 38 | """ 39 | When given a non-existent URL, 40 | we should return a 404 status code 41 | """ 42 | 43 | self.assertEqual(self.client.get("/not-found-url").status_code, 404) 44 | 45 | def test_logout(self): 46 | """ 47 | When given the index URL, 48 | we should return a 302 status code 49 | """ 50 | self.assertEqual(self.client.get("/logout").status_code, 302) 51 | 52 | def test_terms(self): 53 | """ 54 | When given the index URL, 55 | we should return a 200 status code 56 | """ 57 | 58 | self.assertEqual(self.client.get("/terms").status_code, 301) 59 | 60 | def test_privacy(self): 61 | """ 62 | When given the index URL, 63 | we should return a 200 status code 64 | """ 65 | 66 | self.assertEqual(self.client.get("/privacy").status_code, 301) 67 | 68 | def test_docs(self): 69 | """ 70 | When given the index URL, 71 | we should return a 301 status code 72 | """ 73 | 74 | self.assertEqual(self.client.get("/docs").status_code, 301) 75 | -------------------------------------------------------------------------------- /static/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/partial/_navigation.html: -------------------------------------------------------------------------------- 1 | {% macro nav_logo() %} 2 | 14 | {% endmacro %} 15 | 16 | {% macro nav_items() %} 17 | 31 | {% endmacro %} 32 | 33 | Jump to main content 34 | 35 | 60 | -------------------------------------------------------------------------------- /static/js/anbox-stream-docs.md: -------------------------------------------------------------------------------- 1 | # Anbox Stream SDK 2 | 3 | The Anbox Stream SDK is a javascript library you can plug in your website 4 | to easily establish a video stream of your Anbox instances. 5 | 6 | ### Usage 7 | 8 | Include the script 9 | 10 | ```html 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ``` 21 | 22 | 23 | Create a node element with an ID 24 | 25 | ```html 26 |
27 | ``` 28 | 29 | and create a stream instance 30 | 31 | ```javascript 32 | /** 33 | * AnboxStream creates a connection between your client and an Android instance and 34 | * displays its video & audio feed in an HTML5 player 35 | * 36 | * @param options: {object} { 37 | * targetElement: ID of the DOM element to attach the video to. (required) 38 | * url: Address of the service. (required) 39 | * authToken: Authentication token acquired through /1.0/login (required) 40 | * stunServers: List ICE servers (default: [{"urls": ['stun:stun.l.google.com:19302'], username: "", password: ""}]) 41 | * session: { 42 | * app: Android application ID or name. (required) 43 | * }, 44 | * screen: { 45 | * width: screen width (default: 1280) 46 | * height: screen height (default: 720) 47 | * fps: screen frame rate (default: 60) 48 | * density: screen density (default: 240) 49 | * }, 50 | * controls: { 51 | * keyboard: true or false, send keypress events to the Android instance. (default: true) 52 | * mouse: true or false, send mouse and touch events to the Android instance. (default: true) 53 | * gamepad: true or false, send gamepad events to the Android instance. (default: true) 54 | * }, 55 | * callbacks: { 56 | * ready: function, called when the video and audio stream are ready to be inserted. (default: none) 57 | * error: function, called on stream error with the message as parameter. (default: none) 58 | * done: function, called when the stream is closed. (default: none) 59 | * } 60 | * } 61 | */ 62 | 63 | let stream = new AnboxStream({ 64 | targetElement: "anbox-stream", 65 | url: config.backendAddress, 66 | authToken: "abc123", 67 | session: { 68 | app: "some-application-name", 69 | }, 70 | screen: { 71 | width: 720, 72 | height: 1280, 73 | }, 74 | callbacks: { 75 | ready: () => { console.log('video stream is ready') }, 76 | error: (e) => { console.log('an error occurred:', e) }, 77 | done: () => { console.log('stream has been closed') }, 78 | }, 79 | }); 80 | 81 | stream.connect(); 82 | ``` 83 | -------------------------------------------------------------------------------- /templates/partial/_footer.html: -------------------------------------------------------------------------------- 1 | {% if is_docs %} 2 |
3 |
4 |
5 |
6 |

Copyright © {{ now.year }}
CC-BY-SA, Canonical Ltd.

7 |
8 |
9 |
10 |
11 |
12 | 23 |
24 |
25 |
    26 |
  • 27 |

    Android is a trademark of Google LLC. Anbox Cloud uses assets available through the Android Open Source Project.

    28 |
  • 29 |
  • 30 |

    Ubuntu and Canonical are registered trademarks of Canonical Ltd.

    31 |
  • 32 |
33 |
34 |
35 |
36 |
37 |
38 | {% else %} 39 |
40 |
41 |
42 |

© {{ now.year }} Canonical Ltd.

43 |
44 |
45 | 56 |
57 |
58 |
    59 |
  • 60 |

    Android is a trademark of Google LLC. Anbox Cloud uses assets available through the Android Open Source Project.

    61 |
  • 62 |
  • 63 |

    Ubuntu and Canonical are registered trademarks of Canonical Ltd.

    64 |
  • 65 |
66 |
67 |
68 |
69 | {% endif %} 70 | 71 | -------------------------------------------------------------------------------- /static/js/contextualMenu.js: -------------------------------------------------------------------------------- 1 | /** 2 | Toggles the necessary aria- attributes' values on the menus 3 | and handles to show or hide them. 4 | @param {HTMLElement} element The menu link or button. 5 | @param {Boolean} show Whether to show or hide the menu. 6 | @param {Number} top Top offset in pixels where to show the menu. 7 | */ 8 | function toggleMenu(element, show, top) { 9 | var target = document.getElementById(element.getAttribute('aria-controls')); 10 | 11 | if (target) { 12 | element.setAttribute('aria-expanded', show); 13 | target.setAttribute('aria-hidden', !show); 14 | 15 | if (typeof top !== 'undefined') { 16 | target.style.top = top + 'px'; 17 | } 18 | 19 | if (show) { 20 | target.focus(); 21 | } 22 | } 23 | } 24 | 25 | /** 26 | Attaches event listeners for the menu toggle open and close click events. 27 | @param {HTMLElement} menuToggle The menu container element. 28 | */ 29 | function setupContextualMenu(menuToggle) { 30 | menuToggle.addEventListener('click', function (event) { 31 | event.preventDefault(); 32 | var menuAlreadyOpen = menuToggle.getAttribute('aria-expanded') === 'true'; 33 | 34 | var top = menuToggle.offsetHeight; 35 | // for inline elements leave some space between text and menu 36 | if (window.getComputedStyle(menuToggle).display === 'inline') { 37 | top += 5; 38 | } 39 | 40 | toggleMenu(menuToggle, !menuAlreadyOpen, top); 41 | }); 42 | } 43 | 44 | /** 45 | Attaches event listeners for all the menu toggles in the document and 46 | listeners to handle close when clicking outside the menu or using ESC key. 47 | @param {String} contextualMenuToggleSelector The CSS selector matching menu toggle elements. 48 | */ 49 | function setupAllContextualMenus(contextualMenuToggleSelector) { 50 | // Setup all menu toggles on the page. 51 | var toggles = document.querySelectorAll(contextualMenuToggleSelector); 52 | 53 | for (var i = 0, l = toggles.length; i < l; i++) { 54 | setupContextualMenu(toggles[i]); 55 | } 56 | 57 | // Add handler for clicking outside the menu. 58 | document.addEventListener('click', function (event) { 59 | for (var i = 0, l = toggles.length; i < l; i++) { 60 | var toggle = toggles[i]; 61 | var contextualMenu = document.getElementById(toggle.getAttribute('aria-controls')); 62 | var clickOutside = !(toggle.contains(event.target) || contextualMenu.contains(event.target)); 63 | 64 | if (clickOutside) { 65 | toggleMenu(toggle, false); 66 | } 67 | } 68 | }); 69 | 70 | // Add handler for closing menus using ESC key. 71 | document.addEventListener('keydown', function (e) { 72 | e = e || window.event; 73 | 74 | if (e.keyCode === 27) { 75 | for (var i = 0, l = toggles.length; i < l; i++) { 76 | toggleMenu(toggles[i], false); 77 | } 78 | } 79 | }); 80 | } 81 | 82 | setupAllContextualMenus('.p-contextual-menu__toggle'); 83 | -------------------------------------------------------------------------------- /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | # File generated by the Webteam's Yeoman generator: 2 | # generator-canonical-webteam@3.4.3 3 | # 4 | # Default config: https://github.com/sasstools/sass-lint/blob/master/lib/config/sass-lint.yml 5 | # Example config: https://github.com/sasstools/sass-lint/blob/master/docs/sass-lint.yml 6 | 7 | rules: 8 | # Documentation: https://github.com/sasstools/sass-lint/tree/master/docs/rules 9 | 10 | # Strict rules 11 | # === 12 | 13 | # Name formats 14 | class-name-format: 15 | - 2 16 | - 17 | convention: hyphenatedbem 18 | 19 | # Extends and mixins 20 | extends-before-mixins: 2 21 | extends-before-declarations: 2 22 | mixins-before-declarations: 23 | - 2 24 | - 25 | exclude: 26 | - breakpoint 27 | - mq 28 | 29 | # Disallows 30 | no-color-keywords: 2 31 | no-css-comments: 2 32 | no-debug: 2 33 | no-duplicate-properties: 2 34 | no-empty-rulesets: 2 35 | no-invalid-hex: 2 36 | no-mergeable-selectors: 2 37 | no-misspelled-properties: 2 38 | no-trailing-whitespace: 2 39 | no-trailing-zero: 2 40 | 41 | # Line Spacing 42 | one-declaration-per-line: 2 43 | empty-line-between-blocks: 2 44 | single-line-per-selector: 2 45 | 46 | # Disallows 47 | no-qualifying-elements: 48 | - 2 49 | - allow-element-with-attribute: true 50 | 51 | # Name Formats 52 | function-name-format: 2 53 | mixin-name-format: 2 54 | placeholder-name-format: 2 55 | variable-name-format: 2 56 | 57 | # Style Guide 58 | attribute-quotes: 2 59 | bem-depth: 2 60 | border-zero: 2 61 | brace-style: 2 62 | clean-import-paths: 2 63 | empty-args: 2 64 | hex-length: 2 65 | hex-notation: 2 66 | indentation: 2 67 | leading-zero: 2 68 | nesting-depth: 2 69 | pseudo-element: 2 70 | quotes: 0 71 | url-quotes: 2 72 | zero-unit: 2 73 | 74 | # Inner Spacing 75 | space-after-comma: 2 76 | space-before-colon: 2 77 | space-after-colon: 2 78 | space-before-brace: 2 79 | space-before-bang: 2 80 | space-after-bang: 2 81 | space-between-parens: 2 82 | space-around-operator: 2 83 | 84 | # Final Items 85 | trailing-semicolon: 2 86 | final-newline: 2 87 | 88 | # Warnings 89 | # === 90 | 91 | # Extends and mixins 92 | placeholder-in-extend: 1 93 | 94 | # Disallows 95 | no-color-literals: 1 96 | no-transition-all: 1 97 | no-universal-selectors: 0 98 | no-vendor-prefixes: 1 99 | 100 | # Style guide 101 | property-sort-order: 1 102 | 103 | # Disabled rules 104 | # === 105 | 106 | # Disallows 107 | no-attribute-selectors: 0 108 | no-color-hex: 0 109 | no-combinators: 0 110 | no-disallowed-properties: 0 111 | no-extends: 0 112 | no-important: 0 113 | no-warn: 0 114 | property-units: 0 115 | 116 | # Nesting 117 | force-attribute-nesting: 0 118 | force-element-nesting: 0 119 | force-pseudo-nesting: 0 120 | 121 | # Name Formats 122 | id-name-format: 0 123 | 124 | # Style guide 125 | shorthand-values: 0 126 | variable-for-property: 0 127 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: PR checks 2 | on: pull_request 3 | env: 4 | SECRET_KEY: insecure_test_key 5 | 6 | jobs: 7 | build-rock: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout Code 11 | uses: actions/checkout@v4 12 | 13 | - name: Use Node.js 14 | uses: actions/setup-node@v3 15 | 16 | - name: Build Assets 17 | run: | 18 | yarn install 19 | yarn run build 20 | 21 | - name: Setup LXD 22 | uses: canonical/setup-lxd@main 23 | 24 | - name: Setup Rockcraft 25 | run: sudo snap install rockcraft --classic --channel=latest/edge 26 | 27 | - name: Pack Rock 28 | run: rockcraft pack 29 | 30 | run-image: 31 | runs-on: ubuntu-latest 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - name: Build image 37 | run: DOCKER_BUILDKIT=1 docker build --tag anbox-cloud-io . 38 | 39 | - name: Run image 40 | run: | 41 | docker run --detach --env SECRET_KEY=insecure_secret_key --network host anbox-cloud-io 42 | sleep 1 43 | curl --head --fail --retry-delay 1 --retry 30 --retry-connrefused http://localhost 44 | 45 | run-dotrun: 46 | runs-on: ubuntu-latest 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | 51 | - name: Install Dotrun 52 | run: | 53 | sudo pip3 install dotrun requests==2.31.0 # requests version is pinned to avoid breaking changes, can be removed once issue is resolved: https://github.com/docker/docker-py/issues/3256 54 | 55 | - name: Install dependencies 56 | run: | 57 | sudo chmod -R 0777 ../anbox-cloud.io 58 | dotrun install 59 | 60 | - name: Build assets 61 | run: dotrun build 62 | 63 | - name: Test site 64 | run: dotrun & curl --head --fail --retry-delay 1 --retry 30 --retry-connrefused http://localhost:8043 65 | 66 | lint-scss: 67 | runs-on: ubuntu-latest 68 | 69 | steps: 70 | - uses: actions/checkout@v4 71 | 72 | - name: Install dependencies 73 | run: yarn install --immutable 74 | 75 | - name: Lint scss 76 | run: yarn lint-scss 77 | 78 | lint-python: 79 | runs-on: ubuntu-latest 80 | 81 | steps: 82 | - uses: actions/checkout@v4 83 | 84 | - name: Install node dependencies 85 | run: yarn install --immutable 86 | 87 | - name: Install python dependencies 88 | run: | 89 | python3 -m pip install --upgrade pip 90 | sudo pip3 install flake8 black 91 | 92 | - name: Lint python 93 | run: yarn lint-python 94 | 95 | test-python: 96 | runs-on: ubuntu-latest 97 | 98 | steps: 99 | - uses: actions/checkout@v4 100 | 101 | - name: Install dotrun 102 | run: | 103 | sudo pip3 install dotrun requests==2.31.0 # requests version is pinned to avoid breaking changes, can be removed once issue is resolved: https://github.com/docker/docker-py/issues/3256 104 | 105 | - name: Install node dependencies 106 | run: | 107 | sudo chmod -R 0777 ../anbox-cloud.io 108 | dotrun install 109 | 110 | - name: Install dependencies 111 | run: dotrun exec pip3 install coverage 112 | 113 | - name: Build resources 114 | run: dotrun build 115 | 116 | - name: Run tests with coverage 117 | run: dotrun exec coverage run --source=. --module unittest discover tests 118 | 119 | - name: Upload coverage to Codecov 120 | uses: codecov/codecov-action@v3 121 | with: 122 | flags: python 123 | 124 | inclusive-naming-check: 125 | runs-on: ubuntu-latest 126 | 127 | steps: 128 | - name: Checkout 129 | uses: actions/checkout@v4 130 | 131 | - name: woke 132 | uses: canonical-web-and-design/inclusive-naming@main 133 | with: 134 | github-token: ${{ secrets.GITHUB_TOKEN }} 135 | reporter: github-pr-check 136 | fail-on-error: true 137 | -------------------------------------------------------------------------------- /templates/base_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% block title %}Anbox Cloud{% endblock %} 8 | 12 | 13 | 17 | 21 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 54 | 58 | 59 | 60 | 72 | 73 | 74 | 75 | 76 | 77 | 85 | 86 | {% block body %} 87 | {% include "partial/_navigation.html" %} 88 | 89 |
90 | {% block content %}{% endblock content %} 91 |
92 | 93 | {% include "partial/_footer.html" %} 94 | {% endblock %} 95 | 96 | 97 | 98 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy site 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | env: 10 | CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: true 11 | ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: true 12 | 13 | jobs: 14 | pack-charm: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout Code 18 | uses: actions/checkout@v3 19 | 20 | - name: Setup LXD 21 | uses: canonical/setup-lxd@main 22 | 23 | - name: Setup Charmcraft 24 | run: sudo snap install charmcraft --classic --channel=latest/edge 25 | 26 | - name: Fetch libs 27 | run: | 28 | cd ./charm 29 | charmcraft fetch-libs 30 | 31 | - name: Pack charm 32 | run: charmcraft pack -v --project-dir ./charm 33 | 34 | - name: Upload charm 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: anbox-cloud-io-charm 38 | path: ./*.charm 39 | 40 | pack-rock: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Checkout Code 44 | uses: actions/checkout@v3 45 | 46 | - name: Use Node.js 47 | uses: actions/setup-node@v3 48 | 49 | - name: Build Assets 50 | run: | 51 | yarn install 52 | yarn run build 53 | 54 | - name: Setup LXD 55 | uses: canonical/setup-lxd@main 56 | 57 | - name: Setup Rockcraft 58 | run: sudo snap install rockcraft --classic --channel=latest/edge 59 | 60 | - name: Pack Rock 61 | run: rockcraft pack 62 | 63 | - name: Upload Rock 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: anbox-cloud-io-rock 67 | path: ./*.rock 68 | 69 | publish-image: 70 | runs-on: ubuntu-latest 71 | needs: pack-rock 72 | outputs: 73 | image_url: ${{ steps.set_image_url.outputs.image_url }} 74 | steps: 75 | - name: Get Rock 76 | uses: actions/download-artifact@v4 77 | with: 78 | name: anbox-cloud-io-rock 79 | 80 | - name: Set image URL 81 | id: set_image_url 82 | run: echo "image_url=ghcr.io/canonical/anbox-cloud.io:$(date +%s)-${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT 83 | 84 | - name: Push to GHCR 85 | run: skopeo --insecure-policy copy oci-archive:$(ls *.rock) docker://${{ steps.set_image_url.outputs.image_url }} --dest-creds "canonical:${{ secrets.GITHUB_TOKEN }}" 86 | 87 | deploy: 88 | runs-on: [self-hosted, self-hosted-linux-amd64-jammy-private-endpoint-medium] 89 | needs: [pack-charm, publish-image] 90 | steps: 91 | - name: Checkout Code 92 | uses: actions/checkout@v3 93 | 94 | - name: Install Dependencies 95 | run: | 96 | sudo snap install juju --channel=3.4/stable --classic 97 | sudo snap install vault --classic 98 | 99 | - name: Download Charm Artifact 100 | uses: actions/download-artifact@v4 101 | with: 102 | name: anbox-cloud-io-charm 103 | 104 | - name: Configure Vault and Juju 105 | run: | 106 | export VAULT_ADDR=https://vault.admin.canonical.com:8200 107 | export TF_VAR_login_approle_role_id=${{ secrets.VAULT_APPROLE_ROLE_ID }} 108 | export TF_VAR_login_approle_secret_id=${{ secrets.VAULT_APPROLE_SECRET_ID }} 109 | export VAULT_SECRET_PATH_ROLE=secret/prodstack6/roles/prod-anbox-cloud-io 110 | export VAULT_SECRET_PATH_COMMON=secret/prodstack6/juju/common 111 | VAULT_TOKEN=$(vault write -f -field=token auth/approle/login role_id=${TF_VAR_login_approle_role_id} secret_id=${TF_VAR_login_approle_secret_id}) 112 | export VAULT_TOKEN 113 | mkdir -p ~/.local/share/juju 114 | vault read -field=controller_config "${VAULT_SECRET_PATH_COMMON}/controllers/juju-controller-35-production-ps6" | base64 -d > ~/.local/share/juju/controllers.yaml 115 | USERNAME=$(vault read -field=username "${VAULT_SECRET_PATH_ROLE}/juju") 116 | PASSWORD=$(vault read -field=password "${VAULT_SECRET_PATH_ROLE}/juju") 117 | printf "controllers:\n juju-controller-35-production-ps6:\n user: %s\n password: %s\n" "$USERNAME" "$PASSWORD" > ~/.local/share/juju/accounts.yaml 118 | 119 | - name: Deploy Application 120 | run: | 121 | export JUJU_MODEL=admin/prod-anbox-cloud-io 122 | juju refresh anbox-cloud-io --path ./anbox-cloud-io_ubuntu-22.04-amd64.charm --resource flask-app-image=${{ needs.publish-image.outputs.image_url }} 123 | juju wait-for application anbox-cloud-io --query='name=="anbox-cloud-io" && (status=="active" || status=="idle")' 124 | -------------------------------------------------------------------------------- /static/sass/_pattern_strip.scss: -------------------------------------------------------------------------------- 1 | // sass-lint:disable no-color-literals hex-notation 2 | @mixin anbox-p-strip { 3 | @include anbox-p-strip-suru-image; 4 | @include anbox-p-strip-suru-half-and-half-reversed; 5 | } 6 | 7 | @mixin anbox-p-strip-suru-image { 8 | .p-strip--suru-image { 9 | @extend %vf-strip; 10 | 11 | background-blend-mode: multiply, multiply, normal, normal; 12 | background-color: #fff; 13 | // sass-lint:disable-block no-color-literals 14 | background-image: linear-gradient( 15 | to bottom left, 16 | rgba(228, 228, 228, .5) 0%, 17 | rgba(228, 228, 228, .5) 49.9%, 18 | rgba(228, 228, 228, 0) 50%, 19 | rgba(228, 228, 228, 0) 100% 20 | ), 21 | linear-gradient( 22 | to bottom left, 23 | rgba(119, 41, 83, .16) 0%, 24 | rgba(119, 41, 83, .16) 49.9%, 25 | rgba(119, 41, 83, 0) 50%, 26 | rgba(119, 41, 83, 0) 100% 27 | ), 28 | linear-gradient( 29 | to bottom left, 30 | rgba(255, 255, 255, 0) 0%, 31 | rgba(255, 255, 255, 0) 49.6%, 32 | rgba(255, 255, 255, 1) 50%, 33 | rgba(255, 255, 255, 1) 100% 34 | ), 35 | linear-gradient( 36 | -89deg, 37 | rgba(233, 84, 32, 1) 0%, 38 | rgba(119, 41, 83, 1) 38%, 39 | rgba(44, 0, 30, 1) 85% 40 | ); 41 | @supports not (background-blend-mode: multiply) { 42 | background-image: linear-gradient( 43 | to bottom left, 44 | rgba(228, 228, 228, .1) 0%, 45 | rgba(228, 228, 228, .1) 49.9%, 46 | rgba(228, 228, 228, 0) 50%, 47 | rgba(228, 228, 228, 0) 100% 48 | ), 49 | linear-gradient( 50 | to bottom left, 51 | rgba(119, 41, 83, .16) 0%, 52 | rgba(119, 41, 83, .16) 49.9%, 53 | rgba(119, 41, 83, 0) 50%, 54 | rgba(119, 41, 83, 0) 100% 55 | ), 56 | linear-gradient( 57 | to bottom left, 58 | rgba(255, 255, 255, 0) 0%, 59 | rgba(255, 255, 255, 0) 49.6%, 60 | rgba(255, 255, 255, 1) 50%, 61 | rgba(255, 255, 255, 1) 100% 62 | ), 63 | linear-gradient( 64 | -89deg, 65 | rgba(233, 84, 32, 1) 0%, 66 | rgba(119, 41, 83, 1) 38%, 67 | rgba(44, 0, 30, 1) 85% 68 | ); 69 | } 70 | 71 | background-position: right top, right top, right top, right top; 72 | background-repeat: no-repeat; 73 | background-size: 37.7% calc(100% - 6rem), 42.1% calc(100% - 151px), 74 | 49.2% calc(100% - 12rem), 49.2% calc(100% - 12rem); 75 | padding-bottom: 4rem; 76 | padding-top: 4rem; 77 | 78 | @media only screen and (max-width: $breakpoint-small) { 79 | background-blend-mode: normal; 80 | // sass-lint:disable-block no-color-literals 81 | background-image: linear-gradient( 82 | -89deg, 83 | rgba(233, 84, 32, 1) 0%, 84 | rgba(119, 41, 83, 1) 38%, 85 | rgba(44, 0, 30, 1) 85% 86 | ); 87 | background-position: right top; 88 | background-repeat: no-repeat; 89 | background-size: 100% 100%; 90 | color: $color-x-light; 91 | padding-bottom: 4rem; 92 | padding-top: 4rem; 93 | 94 | a:not([class*="p-button"]) { 95 | color: inherit; 96 | font-weight: bold; 97 | text-decoration: underline; 98 | } 99 | } 100 | } 101 | } 102 | 103 | @mixin anbox-p-strip-suru-half-and-half-reversed { 104 | .p-strip--suru-half-and-half-reversed { 105 | @extend %vf-strip; 106 | 107 | background-blend-mode: multiply, normal, normal, normal; 108 | 109 | background-image: linear-gradient( 110 | 25deg, 111 | rgba(119, 41, 83, .16) 0%, 112 | rgba(119, 41, 83, .16) 49.9%, 113 | rgba(119, 41, 83, 0) 50%, 114 | rgba(119, 41, 83, 0) 100% 115 | ), 116 | linear-gradient( 117 | to left, 118 | transparent 0%, 119 | transparent 41.9%, 120 | rgba(255, 255, 255, 0) 42%, 121 | rgba(255, 255, 255, 0) 100% 122 | ), 123 | linear-gradient( 124 | 289deg, 125 | transparent 46.3%, 126 | #a63b3e 20%, 127 | #772953 63%, 128 | #2c001e 100% 129 | ); 130 | background-position: top 60% left, top left, center right; 131 | background-repeat: no-repeat; 132 | background-size: 90% 100%, 100% 100%, cover; 133 | 134 | @media (max-width: $breakpoint-small) { 135 | background-image: linear-gradient( 136 | 270deg, 137 | #e95420 0%, 138 | #772953 43%, 139 | #2c001e 87% 140 | ); 141 | background-position: center right; 142 | background-size: cover; 143 | } 144 | 145 | &.is-light { 146 | color: $color-x-dark; 147 | } 148 | 149 | &.is-dark { 150 | color: $color-x-light; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /templates/modals/contact-us.html: -------------------------------------------------------------------------------- 1 | 113 | 114 | -------------------------------------------------------------------------------- /templates/contact-us.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_layout.html' %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |
8 |

Talk to our experts

9 |
10 |
11 |
12 |
13 |
16 |
17 |

Please briefly tell us about your use case

18 |
    19 |
  • 20 | 25 |
  • 26 |
27 |
28 |
29 |

How should we get in touch?

30 |
    31 |
  • 32 | 33 | 39 |
  • 40 |
  • 41 | 42 | 48 |
  • 49 |
  • 50 | 51 | 58 |
  • 59 |
  • 60 | 61 | 66 |
  • 67 |
  • 68 | 69 | 74 |
  • 75 |
  • 76 | 77 | 83 |
  • 84 | {% include "includes/_country-select.html" %} 85 |
86 |
87 |
88 |
    89 |
  • 90 | 94 | 95 |
  • 96 |
  • 97 | In submitting this form, I confirm that I have read and agree to Canonical's Privacy Policy. 100 |
  • 101 |
102 |
    103 |
  • 104 | 105 |
  • 106 |
107 | 108 | 113 | 118 | 123 |
124 |
125 |
126 |
127 |
128 | 129 | {% endblock content %} 130 | -------------------------------------------------------------------------------- /static/js/modals/dynamic-forms.js: -------------------------------------------------------------------------------- 1 | import setupIntlTelInput from "./intlTelInput.js"; 2 | 3 | (function () { 4 | document.addEventListener("DOMContentLoaded", function () { 5 | var triggeringHash = "#get-in-touch"; 6 | var formContainer = document.getElementById("contact-form-container"); 7 | var contactButtons = document.querySelectorAll(".js-invoke-modal"); 8 | const contactModalSelector = "contact-modal"; 9 | 10 | contactButtons.forEach(function (contactButton) { 11 | contactButton.addEventListener("click", function (e) { 12 | e.preventDefault(); 13 | if (contactButton.dataset.formLocation) { 14 | fetchForm(contactButton.dataset, contactButton); 15 | } else { 16 | fetchForm(formContainer.dataset); 17 | } 18 | open(); 19 | }); 20 | }); 21 | 22 | // Fetch, load and initialise form 23 | function fetchForm(formData, contactButton) { 24 | fetch(formData.formLocation) 25 | .then(function (response) { 26 | return response.text(); 27 | }) 28 | .then(function (text) { 29 | formContainer.classList.remove("u-hide"); 30 | formContainer.innerHTML = text 31 | .replace(/%% formid %%/g, formData.formId) 32 | .replace(/%% returnURL %%/g, formData.returnUrl); 33 | 34 | if (formData.title) { 35 | const title = document.getElementById("modal-title"); 36 | title.innerHTML = formData.title; 37 | } 38 | initialiseForm(); 39 | setFocus(); 40 | }) 41 | .catch(function (error) { 42 | console.log("Request failed", error); 43 | }); 44 | } 45 | 46 | // Open the contact us modal 47 | function open() { 48 | updateHash(triggeringHash); 49 | } 50 | 51 | // Removes the triggering hash 52 | function updateHash(hash) { 53 | var location = window.location; 54 | if (location.hash !== hash || hash === "") { 55 | if ("pushState" in history) { 56 | history.pushState( 57 | "", 58 | document.title, 59 | location.pathname + location.search + hash 60 | ); 61 | } else { 62 | location.hash = hash; 63 | } 64 | } 65 | } 66 | 67 | function initialiseForm() { 68 | var contactIndex = 1; 69 | const contactModal = document.getElementById(contactModalSelector); 70 | var closeModal = document.querySelector(".p-modal__close"); 71 | var closeModalButton = document.querySelector(".js-close"); 72 | var phoneInput = document.querySelector("#phone"); 73 | var modalTrigger = document.activeElement || document.body; 74 | 75 | document.onkeydown = function (evt) { 76 | evt = evt || window.event; 77 | if (evt.keyCode == 27) { 78 | close(); 79 | } 80 | }; 81 | 82 | if (closeModal) { 83 | closeModal.addEventListener("click", function (e) { 84 | e.preventDefault(); 85 | close(); 86 | }); 87 | } 88 | 89 | if (closeModalButton) { 90 | closeModalButton.addEventListener("click", function (e) { 91 | e.preventDefault(); 92 | close(); 93 | }); 94 | } 95 | 96 | if (contactModal) { 97 | let isClickStartedInside = false; 98 | contactModal.addEventListener("mousedown", function (e) { 99 | isClickStartedInside = e.target.id !== contactModalSelector; 100 | }); 101 | contactModal.addEventListener("mouseup", function (e) { 102 | if (!isClickStartedInside && e.target.id === contactModalSelector) { 103 | e.preventDefault(); 104 | close(); 105 | } 106 | }); 107 | } 108 | 109 | // Updates the index and renders the changes 110 | function setState(index) { 111 | contactIndex = index; 112 | } 113 | 114 | // Close the modal and set the index back to the first stage 115 | function close() { 116 | setState(1); 117 | formContainer.classList.add("u-hide"); 118 | formContainer.removeChild(contactModal); 119 | modalTrigger.focus(); 120 | updateHash(""); 121 | } 122 | 123 | // Setup dial code dropdown options (intlTelInput.js) 124 | setupIntlTelInput(phoneInput); 125 | 126 | function fireLoadedEvent() { 127 | var event = new CustomEvent("contactModalLoaded"); 128 | document.dispatchEvent(event); 129 | } 130 | 131 | fireLoadedEvent(); 132 | } 133 | 134 | // Sets the focus inside the modal and trap it 135 | function setFocus() { 136 | var modal = document.querySelector(".p-modal"); 137 | var firstFocusableEle = modal.querySelector( 138 | "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])" 139 | ); 140 | 141 | // set initial focus inside the modal 142 | firstFocusableEle.focus(); 143 | 144 | // trap focus 145 | firstFocusableEle.addEventListener("keydown", function (e) { 146 | if (e.shiftKey && e.key === "Tab") { 147 | e.preventDefault(); 148 | var targetPage = modal.querySelector(".js-pagination:not(.u-hide)"); 149 | var targetEle = targetPage.querySelector(".pagination__link--next"); 150 | targetEle.focus(); 151 | } 152 | }); 153 | } 154 | 155 | // Opens the form when the initial hash matches the trigger 156 | if (window.location.hash === triggeringHash) { 157 | fetchForm(formContainer.dataset); 158 | open(); 159 | } 160 | 161 | // Listens for hash changes and opens the form if it matches the trigger 162 | function locationHashChanged() { 163 | if (window.location.hash === triggeringHash) { 164 | fetchForm(formContainer.dataset); 165 | open(); 166 | } 167 | } 168 | window.onhashchange = locationHashChanged; 169 | }); 170 | })(); 171 | -------------------------------------------------------------------------------- /static/js/docs-side-nav.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | /** 3 | Toggles the expanded/collapsed classed on side navigation element. 4 | 5 | @param {HTMLElement} sideNavigation The side navigation element. 6 | @param {Boolean} show Whether to show or hide the drawer. 7 | */ 8 | function toggleDrawer(sideNavigation, show) { 9 | const toggleButtonOutsideDrawer = sideNavigation.querySelector( 10 | ".p-side-navigation__toggle" 11 | ); 12 | const toggleButtonInsideDrawer = sideNavigation.querySelector( 13 | ".p-side-navigation__toggle--in-drawer" 14 | ); 15 | 16 | if (sideNavigation) { 17 | if (show) { 18 | sideNavigation.classList.remove("is-drawer-collapsed"); 19 | sideNavigation.classList.add("is-drawer-expanded"); 20 | 21 | toggleButtonInsideDrawer.focus(); 22 | toggleButtonOutsideDrawer.setAttribute("aria-expanded", true); 23 | toggleButtonInsideDrawer.setAttribute("aria-expanded", true); 24 | } else { 25 | sideNavigation.classList.remove("is-drawer-expanded"); 26 | sideNavigation.classList.add("is-drawer-collapsed"); 27 | 28 | toggleButtonOutsideDrawer.focus(); 29 | toggleButtonOutsideDrawer.setAttribute("aria-expanded", false); 30 | toggleButtonInsideDrawer.setAttribute("aria-expanded", false); 31 | } 32 | } 33 | } 34 | 35 | // throttle util (for window resize event) 36 | var throttle = function (fn, delay) { 37 | var timer = null; 38 | return function () { 39 | var context = this, 40 | args = arguments; 41 | clearTimeout(timer); 42 | timer = setTimeout(function () { 43 | fn.apply(context, args); 44 | }, delay); 45 | }; 46 | }; 47 | 48 | /** 49 | Attaches event listeners for the side navigation toggles 50 | @param {HTMLElement} sideNavigation The side navigation element. 51 | */ 52 | function setupSideNavigation(sideNavigation) { 53 | var toggles = [].slice.call( 54 | sideNavigation.querySelectorAll(".js-drawer-toggle") 55 | ); 56 | var drawerEl = sideNavigation.querySelector(".p-side-navigation__drawer"); 57 | 58 | // hide navigation drawer on small screens 59 | sideNavigation.classList.add("is-drawer-hidden"); 60 | 61 | // setup drawer element 62 | drawerEl.addEventListener("animationend", () => { 63 | if (!sideNavigation.classList.contains("is-drawer-expanded")) { 64 | sideNavigation.classList.add("is-drawer-hidden"); 65 | } 66 | }); 67 | 68 | window.addEventListener("keydown", (e) => { 69 | if (e.key === "Escape") { 70 | toggleDrawer(sideNavigation, false); 71 | } 72 | }); 73 | 74 | // setup toggle buttons 75 | toggles.forEach(function (toggle) { 76 | toggle.addEventListener("click", function (event) { 77 | event.preventDefault(); 78 | 79 | if (sideNavigation) { 80 | sideNavigation.classList.remove("is-drawer-hidden"); 81 | toggleDrawer( 82 | sideNavigation, 83 | !sideNavigation.classList.contains("is-drawer-expanded") 84 | ); 85 | } 86 | }); 87 | }); 88 | 89 | // hide side navigation drawer when screen is resized 90 | window.addEventListener( 91 | "resize", 92 | throttle(function () { 93 | toggles.forEach((toggle) => { 94 | return toggle.setAttribute("aria-expanded", false); 95 | }); 96 | // remove expanded/collapsed class names to avoid unexpected animations 97 | sideNavigation.classList.remove("is-drawer-expanded"); 98 | sideNavigation.classList.remove("is-drawer-collapsed"); 99 | sideNavigation.classList.add("is-drawer-hidden"); 100 | }, 10) 101 | ); 102 | } 103 | 104 | /** 105 | Attaches event listeners for all the side navigations in the document. 106 | @param {String} sideNavigationSelector The CSS selector matching side navigation elements. 107 | */ 108 | function setupSideNavigations(sideNavigationSelector) { 109 | // Setup all side navigations on the page. 110 | var sideNavigations = [].slice.call( 111 | document.querySelectorAll(sideNavigationSelector) 112 | ); 113 | 114 | sideNavigations.forEach(setupSideNavigation); 115 | } 116 | 117 | setupSideNavigations('.p-side-navigation, [class*="p-side-navigation--"]'); 118 | 119 | // Setup expandable side navigation 120 | 121 | var expandToggles = document.querySelectorAll(".p-side-navigation__expand"); 122 | var navigationLinks = document.querySelectorAll(".p-side-navigation__link"); 123 | 124 | // setup default values of aria-expanded for the toggle button, list title and list itself 125 | const setup = (toggle) => { 126 | const isExpanded = toggle.getAttribute("aria-expanded") === "true"; 127 | if (!isExpanded) { 128 | toggle.setAttribute("aria-expanded", isExpanded); 129 | } 130 | const item = toggle.closest(".p-side-navigation__item"); 131 | const link = item.querySelector(".p-side-navigation__link"); 132 | const nestedList = item.querySelector(".p-side-navigation__list"); 133 | if (!link?.hasAttribute("aria-expanded")) { 134 | link.setAttribute("aria-expanded", isExpanded); 135 | } 136 | if (!nestedList?.hasAttribute("aria-expanded")) { 137 | nestedList.setAttribute("aria-expanded", isExpanded); 138 | } 139 | }; 140 | 141 | const handleToggle = (e) => { 142 | const item = e.currentTarget.closest(".p-side-navigation__item"); 143 | const button = item.querySelector(".p-side-navigation__expand"); 144 | const link = item.querySelector(".p-side-navigation__link"); 145 | const nestedList = item.querySelector(".p-side-navigation__list"); 146 | [button, link, nestedList].forEach((el) => 147 | el.setAttribute( 148 | "aria-expanded", 149 | el.getAttribute("aria-expanded") === "true" ? "false" : "true" 150 | ) 151 | ); 152 | }; 153 | 154 | expandToggles.forEach((toggle) => { 155 | setup(toggle); 156 | toggle.addEventListener("click", (e) => { 157 | handleToggle(e); 158 | }); 159 | }); 160 | })(); 161 | -------------------------------------------------------------------------------- /webapp/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from urllib.parse import urlparse 3 | from datetime import datetime 4 | 5 | import flask 6 | import requests 7 | from flask import request 8 | 9 | from canonicalwebteam.flask_base.app import FlaskBase 10 | from flask_openid import OpenID 11 | from pymacaroons import Macaroon 12 | from webapp.macaroons import MacaroonRequest, MacaroonResponse 13 | from posixpath import join as url_join 14 | from canonicalwebteam import image_template 15 | 16 | from webapp.views import get_user_country_by_ip 17 | 18 | LOGIN_URL = "https://login.ubuntu.com" 19 | ANBOXCLOUD_API_BASE = "https://demo-api.anbox-cloud.io" 20 | 21 | session = requests.Session() 22 | app = FlaskBase( 23 | __name__, 24 | "anbox-cloud.io", 25 | template_folder="../templates", 26 | static_folder="../static", 27 | template_404="404.html", 28 | template_500="500.html", 29 | ) 30 | 31 | app.secret_key = os.environ["SECRET_KEY"] 32 | open_id = OpenID( 33 | store_factory=lambda: None, 34 | safe_roots=[], 35 | extension_responses=[MacaroonResponse], 36 | ) 37 | 38 | 39 | app.add_url_rule("/user-country.json", view_func=get_user_country_by_ip) 40 | 41 | 42 | @app.context_processor 43 | def inject_now(): 44 | return {"now": datetime.utcnow()} 45 | 46 | 47 | def _api_request(url_path, method="GET", params=None, json=None, headers=None): 48 | """ 49 | Make API calls to the anbox API, passing any 401 errors to Flask to handle 50 | """ 51 | 52 | response = session.request( 53 | method, 54 | f"{ANBOXCLOUD_API_BASE.rstrip('/')}/{url_join(url_path).lstrip('/')}", 55 | params=params, 56 | json=json, 57 | headers=headers, 58 | ) 59 | 60 | if response.status_code == 401: 61 | flask.abort(401, response.json()) 62 | 63 | response.raise_for_status() 64 | 65 | return response.json() 66 | 67 | 68 | def login_required(func): 69 | """ 70 | Decorator that checks if a user is logged in, and redirects 71 | to login page if not. 72 | """ 73 | 74 | def is_user_logged_in(*args, **kwargs): 75 | if "authentication_token" not in flask.session: 76 | return flask.redirect("/login?next=" + flask.request.path) 77 | 78 | # Validate authentication token 79 | return func(*args, **kwargs) 80 | 81 | return is_user_logged_in 82 | 83 | 84 | @app.context_processor 85 | def utility_processor(): 86 | return {"image": image_template} 87 | 88 | 89 | @app.route("/") 90 | def index(): 91 | return flask.render_template("index.html") 92 | 93 | 94 | @app.route("/contact-us") 95 | def contact_us(): 96 | return flask.render_template("contact-us.html") 97 | 98 | 99 | @app.route("/modals/contact-us") 100 | def modals_contact_us(): 101 | return flask.render_template("/modals/contact-us.html") 102 | 103 | 104 | @app.route("/thank-you") 105 | def thank_you(): 106 | return flask.render_template("thank-you.html") 107 | 108 | 109 | @app.route("/terms") 110 | def terms(): 111 | return flask.render_template("terms.html") 112 | 113 | 114 | @app.route("/privacy") 115 | def privacy(): 116 | return flask.render_template("privacy.html") 117 | 118 | 119 | @app.route("/sitemap.xml") 120 | def sitemap_index(): 121 | xml_sitemap = flask.render_template("sitemap/sitemap-index.xml") 122 | response = flask.make_response(xml_sitemap) 123 | response.headers["Content-Type"] = "application/xml" 124 | 125 | return response 126 | 127 | 128 | @app.route("/sitemap-links.xml") 129 | def sitemap_links(): 130 | xml_sitemap = flask.render_template("sitemap/sitemap-links.xml") 131 | response = flask.make_response(xml_sitemap) 132 | response.headers["Content-Type"] = "application/xml" 133 | 134 | return response 135 | 136 | 137 | @open_id.after_login 138 | def after_login(resp): 139 | """ 140 | 1. Get Macaroon discharge 141 | 2. Post payload with discharge and root (API requirements) 142 | 3. Empty session and add new token for Anbox-cloud API 143 | """ 144 | 145 | root = flask.session["macaroon_root"] 146 | discharge = resp.extensions["macaroon"].discharge 147 | data = { 148 | "provider": "usso", 149 | "authorization_code": f"root={root} discharge={discharge}", 150 | "invitation_code": flask.session["invitation_code"], 151 | "accept_tos": True, 152 | } 153 | response = _api_request("1.0/login", method="POST", json=data) 154 | flask.session.pop("macaroon_root", None) 155 | flask.session["authentication_token"] = response["metadata"]["token"] 156 | 157 | return flask.redirect(open_id.get_next_url()) 158 | 159 | 160 | @app.after_request 161 | def add_headers(response): 162 | """ 163 | Generic rules for headers to add to all requests 164 | 165 | - X-Hostname: Mention the name of the host/pod running the application 166 | - Cache-Control: Add cache-control headers for public and private pages 167 | """ 168 | 169 | if response.status_code == 200: 170 | if flask.session: 171 | response.headers["Cache-Control"] = "private" 172 | else: 173 | # Only add caching headers to successful responses 174 | if not response.headers.get("Cache-Control"): 175 | response.headers["Cache-Control"] = ", ".join( 176 | { 177 | "public", 178 | "max-age=61", 179 | "stale-while-revalidate=300", 180 | "stale-if-error=86400", 181 | } 182 | ) 183 | 184 | return response 185 | 186 | 187 | @app.route("/logout") 188 | def logout(): 189 | """ 190 | Logout by removing the `authentication_token` from the session 191 | """ 192 | flask.session.pop("authentication_token", None) 193 | 194 | return flask.redirect(open_id.get_next_url()) 195 | 196 | 197 | @app.route("/login", methods=["GET", "POST"]) 198 | @open_id.loginhandler 199 | def login_handler(): 200 | if "authentication_token" in flask.session: 201 | return flask.redirect(open_id.get_next_url()) 202 | flask.session["invitation_code"] = request.args.get("invitation_code") 203 | response = _api_request( 204 | url_path="/1.0/token", method="GET", params={"provider": "usso"} 205 | ) 206 | root = response["metadata"]["token"] 207 | location = urlparse(LOGIN_URL).hostname 208 | (caveat,) = [ 209 | c 210 | for c in Macaroon.deserialize(root).third_party_caveats() 211 | if c.location == location 212 | ] 213 | openid_macaroon = MacaroonRequest(caveat_id=caveat.caveat_id) 214 | 215 | flask.session["macaroon_root"] = root 216 | return open_id.try_login( 217 | LOGIN_URL, ask_for=["email"], extensions=[openid_macaroon] 218 | ) 219 | 220 | 221 | @app.errorhandler(401) 222 | def handle_unauthorised(error): 223 | """ 224 | Handle 401 errors using flask as opposed to requests 225 | """ 226 | if error.description["error_code"] == 900: 227 | flask.session.pop("authentication_token", None) 228 | return flask.redirect("/login?next=" + flask.request.path) 229 | 230 | return ( 231 | flask.render_template("401.html", error=error.description["error"]), 232 | 401, 233 | ) 234 | -------------------------------------------------------------------------------- /templates/terms.html: -------------------------------------------------------------------------------- 1 |

Terms of Service

2 | 3 |

I agree that my use of the services are subject to my adherence to, and acceptance of, the Terms of Service*, Privacy Notice and Privacy Policy. 4 | TERMS OF SERVICE 5 | 1. Introduction 6 | These Terms of Service cover your access to the Anbox Cloud Demo Service (“Anbox Cloud Demo”) made available by Canonical Group Limited ("Canonical", "us", "our" or "we") to you ("you" or "your") subject to and in accordance with these Terms of Service (the “Service”). 7 | 8 | Please read these Terms of Service carefully before you use the Services. By using Services, you agree to become bound by these Terms of Service. You may not use the Service for illegal or unauthorised purposes or otherwise in accordance with these Terms of Service. 9 | 10 | If you are entering into these Terms of Service on behalf of a company or legal entity, you represent that you have authority to bind such company or legal entity, its officers, employees and agents and all users who access the Service through the account, to these Terms of Service. “you” and “your” shall refer to the company or legal entity and such users. If you do not have such authority do not accept these Terms of Service. 11 | 12 | You must be at least 13 years old to use the Service. If you are between age 13 and 18, we require your parent's or legal guardian's consent to use of the Services, please contact us at legal@canonical.com. 13 | 14 | Any change or update to the Services or Terms of Service will be made in accordance with section 6 below. 15 | 16 | 2. Services 17 | The Service enables you to access a demo of Anbox Cloud (https://anbox-cloud.io) which includes streaming an Android system from the cloud. 18 | 19 | 3. Account 20 | You will need an account to access and use the Service. We reserve the right to reject your request for an account or to immediately cancel or suspend your account and your use of the Services at any time if you do not comply with the following requirements. You must not attempt to create an account or use the Service if doing so would violate these Terms of Service. You must: 21 | 22 | have a Service account 23 | not use the Service for illegal or unauthorised purposes (or which encourage or permit illegal or authorised purposes), in infringement of a third party's’ rights or otherwise in accordance with these Terms of Service 24 | not take any action or use the Service in any way that might bring Canonical into disrepute or affect the ability of Canonical to provide the Service 25 | not use the Service in any manner that might be libellous or defamatory, that contains threats or incites violence towards individuals or entities, or that violates the privacy rights of any third party 26 | not be located in or use the Services in an OFAC/EAR embargoed or sanctioned country or be on the U.S. Commerce Department’s Denied Persons List, Entity List, or Unverified List 27 | 28 | In order to access the Anbox Cloud Demo you will require an invitation code and a Single Sign On account with Canonical. If you do not have an account you will need to create one. You are responsible for choosing an appropriate password for your accounts and for keeping such password secure. Canonical will not ask you for your password and you should not reveal it to anyone. You are responsible for keeping your account details up to date. 29 | 30 | 4. End of Service 31 | 32 | We look forward to providing you with the Service for a limited trial period only. The trial period will be specified on the invitation code and may differ from user to user. However, there are also some circumstances under which the Service may be suspended or terminated: 33 | 34 | We cease to make the Service (or any part thereof) available 35 | By us at any time as described in Section 3 above 36 | 37 | We may also cease to offer the Services for any other reason, in which case we will provide you with notice on your account page. 38 | 39 | 6. Changes 40 | 41 | We aim to continually improve the delivery and content of the Service and accordingly may make changes to the Service throughout the trial. 42 | 43 | This Service is offered on a trial basis, and your use of this Service will be limited to the period of your trial. New features may be added, but we may also modify or discontinue (temporarily or permanently) part of the Service, in part or in whole before the end of the trial period. 44 | 45 | In the event of a material change to the Service, we will notify you on your account page. What constitutes a material change in this circumstance will be determined by Canonical, in good faith and using common sense and reasonable judgment. 46 | 47 | Similarly, we may occasionally make changes to these Terms of Service and will notify you of material changes either by email or on your account page. If Canonical does make changes to these Terms of Service, all changes will go into effect at the time we post the updated Terms of Service on your account page or as otherwise notified at that time. 48 | 7. Intellectual property 49 | 50 | You have a non-exclusive, non-transferable (to the extent permitted by law) right to view, access and use the Services for such time as it is made available by us strictly in accordance with these Terms of Service. 51 | 52 | You will not acquire any rights to the Service (or the intellectual property rights contained therein) from your use of the Service, other than as set out in these Terms of Service. 53 | 54 | 8. Personal data 55 | 56 | In order to provide the Service to you, you will be required to provide information about yourself such as your name, address, job title and company. Any such information you provide to Canonical must always be accurate, correct and up to date. 57 | 58 | Our Privacy Notice and Privacy Policy explain how we treat your personal data and protect your privacy when using the Services and our services in general. Canonical may provide limited personal data, such as your name, email address and related information, to third parties Ampere Computing LLC with a registered office at 4655 Great America Pkwy Suite 601 Santa Clara, CA 95054, United States and Packet Host Inc., with a registered office in 30 Vesey Street, New York, 10007 NY, United States. By accessing the Anbox Cloud Demo you are agreeing to the use of your personal data in this way. 59 | 60 | We may also collect certain non-personally-identifiable information, which is located on your computer. The information collected may include statistics relating to how often data is transferred, and performance metrics in relation to software and configuration. You agree this information may be retained and used by Canonical. 61 | 62 | Canonical may disclose any or all personal data and contents you have sent, posted or published if required to comply with applicable law or the order or requirement of a court, administrative agency or other governmental body. All other use of your personal data is subject to the Privacy Policy. 63 | 9. Liability 64 | Your use of the Service is at your sole risk. The Service is provided on an “as is” and “as available” basis without warranty of any kind. 65 | 66 | In the case of the Service provided by Canonical, the Service is provided “as is” and, other than as expressly set out in these Terms of Service, all warranties (whether express, implied, statutory or otherwise) in respect of the Services are expressly excluded to the maximum extent permitted by law. 67 | 68 | Canonical will provide the Services with reasonable care and skill, and Canonical will use reasonable efforts to ensure the availability of the Services, but makes no guarantee that the Services will be available without interruption or will be error-free. 69 | 70 | Canonical will not be liable in contract, tort or otherwise for any: indirect or consequential loss; loss of profits; loss of revenue; loss of anticipated savings; loss of business or business opportunity; loss of goodwill; or loss of or corruption to data. Otherwise, Canonical’s total liability in contract, tort or otherwise for any claims is limited to £10. 71 | 72 | Nothing in these Terms of Service will exclude or limit Canonical’s liability for: death or personal injury caused by the negligence of Canonical; fraud or fraudulent misrepresentation; or any other liability that cannot be excluded or limited by law. 73 | 74 | 9. General 75 | These Terms of Service are governed by the laws of England and any dispute will be heard by the courts in England. 76 | 77 | Failure by Canonical to enforce any right or provision of these Terms of Service shall not constitute a waiver of such right or provision. If any part of these Terms of Service is held invalid or unenforceable, that part will be construed to reflect the parties original intent, and the remaining portions will remain in full force and effect. The terms of these Terms of Service do not affect your statutory rights. 78 | 79 | Any notices should be sent by email to legal@canonical.com or by registered post to: 80 | 81 | Canonical Group Limited, 82 | 5 New Street Square, 83 | London, 84 | EC4A 3TW. 85 | 86 | Version: January 2020 87 | 88 |

-------------------------------------------------------------------------------- /templates/includes/_country-select.html: -------------------------------------------------------------------------------- 1 | {% if raw != "true" %} 2 |
  • 3 | 4 | {% endif %} 5 | 263 | {% if raw != "true" %}
  • {% endif %} -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # CAUTION: 4 | # This file was generated by generator-canonical-webteam@3.4.3 5 | # and should usually not be edited directly. 6 | # 7 | # This file was generated by the "canonical-webteam" Yeoman generator 8 | # https://npmjs.org/package/generator-canonical-webteam 9 | # 10 | # Update it to the latest version with: 11 | # 12 | # $ sudo npm install -g yo generator-canonical-webteam 13 | # $ yo canonical-webteam:run 14 | 15 | set -euo pipefail 16 | 17 | USAGE="How to use ./run v3.4.3 18 | === 19 | 20 | $ ./run \\ 21 | [-e|--env VAR_NAME=value] # Declare an environment variable to use while running commands \\ 22 | [-m|--node-module PATH] # A path to a local node module to use instead of the installed dependencies \\ 23 | [COMMAND] # Optionally provide a command to run 24 | 25 | If no COMMAND is provided, \`serve\` will be run. 26 | 27 | Commands 28 | --- 29 | 30 | - serve [-p|--port PORT] [-d|--detach] [-f|--forward-port]: Run a development server 31 | - watch [-s|--watch-site]: Run \`yarn run watch\` (for jekyll sites, watch for changes with \`--watch-site\`) 32 | - build: Run \`yarn run build\` 33 | - test: Run \`yarn run test\` 34 | - test-python: Run \`yarn run test-python\` 35 | - lint-python: Run \`yarn run lint-python\` 36 | - stop: Stop any running containers 37 | - exec [-r|--root] [-p|--expose-port PORT] : Run a command in the development container (optionally exposing a port to the host) 38 | - yarn [-r|--root] [-p|--expose-port PORT] : Run a yarn script from package.json 39 | - clean: Remove all images and containers, any installed dependencies and the .docker-project file 40 | - clean-cache: Empty cache files, which are saved between projects (eg, yarn) 41 | " 42 | 43 | ## 44 | # Variable definitions 45 | ## 46 | 47 | # Define docker images versions 48 | dev_image="canonicalwebteam/dev:v1.6.7" 49 | if [ -n "${DOCKER_REGISTRY:-}" ]; then 50 | dev_image="${DOCKER_REGISTRY}/${dev_image}" 51 | fi 52 | 53 | # Interactivity options 54 | [ -t 1 ] && tty="--tty --interactive" || tty="" # Do we have a terminal? 55 | [ -f .env ] && env_file="--env-file .env" || env_file="" # Do we have an env file? 56 | [ -f .env.local ] && env_file="${env_file} --env-file .env.local" || env_file=$env_file # Do we have a local env file? 57 | 58 | # Defaults environment settings 59 | PORT=8000 60 | 61 | # Import environment settings 62 | if [ -f .env ]; then 63 | source .env 64 | fi 65 | 66 | # Other variables 67 | run_serve_docker_opts="${CANONICAL_WEBTEAM_RUN_SERVE_DOCKER_OPTS:-}" 68 | module_volumes="" 69 | env_vars="" 70 | 71 | # Decide which md5 command to use 72 | if $(command -v md5sum > /dev/null); then md5_command="md5sum"; 73 | elif $(command -v md5 > /dev/null); then md5_command="md5"; 74 | else echo "No md5 tool available. Exiting."; exit 1; fi 75 | 76 | ## 77 | # Check docker is installed correctly 78 | ## 79 | if ! command -v docker >/dev/null 2>&1; then 80 | echo " 81 | Error: Docker not installed 82 | == 83 | Please install Docker before continuing: 84 | https://www.docker.com/products/docker 85 | " 86 | exit 1 87 | fi 88 | if grep -q '^docker:' /etc/group && ! groups | grep -q '\bdocker\b'; then 89 | echo " 90 | Error: `whoami` not in docker group 91 | === 92 | Please add this user to the docker group, e.g. with: 93 | \$ newgrp docker 94 | " 95 | exit 1 96 | fi 97 | 98 | # Grab HTTP_PROXY settings from the host 99 | http_proxy="" 100 | yarn_proxy="" 101 | if [ -n "${HTTP_PROXY:-}" ]; then 102 | http_proxy="--env HTTP_PROXY=${HTTP_PROXY} --env http_proxy=${HTTP_PROXY}" 103 | yarn_proxy="--proxy ${HTTP_PROXY}" 104 | fi 105 | if [ -n "${HTTPS_PROXY:-}" ]; then 106 | http_proxy="${http_proxy} --env HTTPS_PROXY=${HTTPS_PROXY} --env https_proxy=${HTTPS_PROXY}" 107 | yarn_proxy="${yarn_proxy} --https-proxy ${HTTPS_PROXY}" 108 | fi 109 | 110 | # Generate the project name 111 | if [[ -f ".docker-project" ]]; then 112 | project=$(cat .docker-project) 113 | else 114 | directory="$(basename "$(pwd)")" 115 | hash=$(pwd | ${md5_command} | cut -c1-8) 116 | project=canonical-webteam-${directory}-${hash} 117 | echo $project > .docker-project 118 | fi 119 | 120 | # Volume names 121 | cache_volume="${CANONICAL_WEBTEAM_CACHE_VOLUME:-canonical-webteam-cache}" 122 | etc_volume="${project}-etc" 123 | usr_local_volume="${project}-usr-local" 124 | db_volume="${project}-db" 125 | 126 | # Container names 127 | db_container="${project}-db" 128 | pip_container="${project}-pip" 129 | 130 | # Network name 131 | network_name="${project}-net" 132 | 133 | invalid() { 134 | message=${1} 135 | echo "Error: ${message}" 136 | echo "" 137 | echo "$USAGE" 138 | exit 1 139 | } 140 | 141 | # Read optional arguments 142 | while [[ -n "${1:-}" ]] && [[ "${1:0:1}" == "-" ]]; do 143 | key="$1" 144 | 145 | case $key in 146 | -e|--env) 147 | if [ -z "${2:-}" ]; then invalid "Missing environment variables. Usage: --env XXXX=yyyy"; fi 148 | env_vars="${env_vars} --env ${2}" 149 | shift 150 | ;; 151 | -m|--node-module) 152 | if [ -z "${2:-}" ]; then invalid "Missing module name. Usage: --node-module ."; fi 153 | # Ensure directories exist, ready to host module volumes 154 | if [ ! -d "`pwd`/node_modules/$(basename ${2})" ]; then 155 | mkdir -p "`pwd`/node_modules/$(basename ${2})" 156 | fi 157 | module_volumes="${module_volumes} --volume=${2}:`pwd`/node_modules/$(basename ${2})" 158 | shift 159 | ;; 160 | -h|--help) echo "$USAGE"; exit ;; 161 | -v|--version) echo "Generated from generator-canonical-webteam@3.4.3"; exit ;; 162 | *) invalid "Option '${key}' not recognised." ;; 163 | esac 164 | shift 165 | done 166 | 167 | start_django_db () { 168 | # Run the database if necessary 169 | if grep -q django.db.backends.postgresql_psycopg2 */settings.py 2> /dev/null; then 170 | # Create isolated network 171 | if ! docker network inspect ${network_name} &> /dev/null; then 172 | docker network create ${network_name} 173 | fi 174 | 175 | # Start the database 176 | trap "kill_container ${db_container}" EXIT; 177 | if ! docker inspect -f {{.State.Running}} ${db_container} &>/dev/null; then 178 | docker run \ 179 | --name ${db_container} `# Name the container` \ 180 | --rm `# Remove the container once it's finished` \ 181 | --volume "${db_volume}":/var/lib/postgresql/data `# Store dependencies in a docker volume` \ 182 | ${http_proxy} `# Include HTTP proxy if needed` \ 183 | --network ${network_name} `# Use an isolated network` \ 184 | --network-alias db `# Call this container "db" on the network so it can be found` \ 185 | --detach `# Run in the background` \ 186 | postgres `# Use the image for node version 7` 187 | fi 188 | 189 | # Wait for it 190 | wait_time=0 191 | until docker exec ${db_container} pg_isready || [ $wait_time -eq 4 ]; do 192 | sleep $(( wait_time++ )) 193 | done 194 | 195 | # Provision database 196 | run_as_user "${network}" python3 manage.py migrate 197 | fi 198 | } 199 | 200 | kill_container () { 201 | container_name="${1}" 202 | 203 | # Kill any previous containers 204 | previous_id=$(docker ps --all --quiet --filter "name=^/${container_name}$") 205 | if [ -n "${previous_id}" ]; then 206 | docker rm --force ${previous_id} > /dev/null; 207 | fi 208 | } 209 | 210 | docker_run () { 211 | # Get options 212 | docker_run_options="${1}"; shift 213 | 214 | # Generate container name from command 215 | container_name="${project}-${@}" 216 | container_name="${container_name// /_}" # Replace spaces with underscores 217 | container_name=$(echo ${container_name} | tr -dc '[:alnum:]_.-') # Remove disallowed chars 218 | 219 | # Use network if it's been setup 220 | network="" 221 | if docker network inspect ${network_name} &> /dev/null; then 222 | network="--network ${network_name}" 223 | fi 224 | 225 | # Kill existing containers 226 | kill_container "${container_name}" 227 | 228 | # Environment info 229 | commit_id=$(git rev-parse HEAD || echo "unknown") 230 | 231 | # Start the new container 232 | docker run \ 233 | --name ${container_name} `# Name the container` \ 234 | --rm `# Remove the container once it's finished` \ 235 | --volume "$(pwd):$(pwd)" `# Mirror current directory inside container` \ 236 | --workdir "$(pwd)" `# Set current directory to the image's work directory` \ 237 | --volume ${etc_volume}:/etc `# Use etc with corresponding user added` \ 238 | --volume ${usr_local_volume}:/usr/local/ `# Bind local folder to volume` \ 239 | --volume ${cache_volume}:/home/shared/.cache/ `# Bind cache to volume` \ 240 | --env COMMIT_ID=${commit_id} `# Pass through the commit ID` \ 241 | ${network} `# Network settings, if needed` \ 242 | ${env_file} `# Pass any files of environment variables to the container` \ 243 | ${env_vars} `# Pass explicit environment variables to the container` \ 244 | ${http_proxy} `# Include HTTP proxy if needed` \ 245 | ${tty} `# Attach a pseudo-terminal, if relevant` \ 246 | ${docker_run_options} `# Extra options` \ 247 | ${dev_image} $@ `# Run command in the image` 248 | } 249 | 250 | run_as_user () { 251 | run_as_user_options="${1}"; shift 252 | 253 | create_etc_volume 254 | 255 | docker_run "--user $(id -u):$(id -g) ${run_as_user_options}" $@ 256 | } 257 | 258 | create_etc_volume() { 259 | # Create local user and group in the dev image 260 | uid=$(id -u) 261 | gid=$(id -g) 262 | 263 | if ! docker volume inspect -f " " ${etc_volume} 2> /dev/null; then 264 | etc_run="docker run --rm --volume ${etc_volume}:/etc ${dev_image}" 265 | if ! ${etc_run} grep -P "${gid}:$" /etc/group; then 266 | ${etc_run} groupadd -g ${gid} app-user 267 | fi 268 | 269 | if ! ${etc_run} grep -P "x:${uid}:" /etc/passwd; then 270 | ${etc_run} useradd -u ${uid} -g ${gid} app-user 271 | fi 272 | fi 273 | } 274 | 275 | update_dependencies() { 276 | # Make sure the etc volume has been created first 277 | create_etc_volume 278 | 279 | # Install yarn dependencies 280 | if [ -f package.json ]; then 281 | package_json_hash=$(${md5_command} package.json | cut -c1-8) 282 | if [ -d node_modules ]; then 283 | yarn_dependencies_hash=$(find node_modules -type f ! -wholename 'node_modules/.cache/*' -print0 | sort -z | xargs -0 ${md5_command} | ${md5_command} | cut -c1-8)-${package_json_hash} 284 | fi 285 | if [ -z "${yarn_dependencies_hash:-}" ] || [ ! -f .yarn.${project}.hash ] || [ "${yarn_dependencies_hash}" != "$(cat .yarn.${project}.hash)" ]; then 286 | echo "Installing new Yarn dependencies" 287 | run_as_user "" yarn install --force ${yarn_proxy} 288 | yarn_dependencies_hash=$(find node_modules -type f ! -wholename 'node_modules/.cache/*' -print0 | sort -z | xargs -0 ${md5_command} | ${md5_command} | cut -c1-8)-${package_json_hash} 289 | echo ${yarn_dependencies_hash} > .yarn.${project}.hash 290 | echo "Saved ${yarn_dependencies_hash} to .yarn.${project}.hash" 291 | else 292 | echo "Yarn dependencies haven't changed. To force an update, delete .yarn.${project}.hash." 293 | fi 294 | fi 295 | 296 | # Install bower dependencies 297 | if [ -f bower.json ]; then 298 | bower_json_hash=$(${md5_command} bower.json | cut -c1-8) 299 | if [ -d bower_components ]; then 300 | bower_dependencies_hash=$(find bower_components -type f -print0 | sort -z | xargs -0 ${md5_command} | ${md5_command} | cut -c1-8)-${bower_json_hash} 301 | fi 302 | if [ -z "${bower_dependencies_hash:-}" ] || [ ! -f .bower.${project}.hash ] || [ "${bower_dependencies_hash}" != "$(cat .bower.${project}.hash)" ]; then 303 | echo "Installing new bower dependencies" 304 | run_as_user "" bower install 305 | bower_dependencies_hash=$(find bower_components -type f -print0 | sort -z | xargs -0 ${md5_command} | ${md5_command} | cut -c1-8)-${bower_json_hash} 306 | echo ${bower_dependencies_hash} > .bower.${project}.hash 307 | else 308 | echo "Bower dependencies haven't changed. To force an update, delete .bower.${project}.hash." 309 | fi 310 | fi 311 | 312 | # Install ruby dependencies 313 | if [ -f Gemfile ]; then 314 | gemfile_hash=$(${md5_command} Gemfile | cut -c1-8) 315 | if [ -d vendor/bundle ]; then 316 | bundler_dependencies_hash=$(find vendor/bundle -type f -print0 | sort -z | xargs -0 ${md5_command} | ${md5_command} | cut -c1-8)-${gemfile_hash} 317 | fi 318 | if [ -z "${bundler_dependencies_hash:-}" ] || [ ! -f .bundler.${project}.hash ] || [ "${bundler_dependencies_hash}" != "$(cat .bundler.${project}.hash)" ]; then 319 | echo "Installing new bundler dependencies" 320 | run_as_user "" bundle install --path vendor/bundle 321 | bundler_dependencies_hash=$(find vendor/bundle -type f -print0 | sort -z | xargs -0 ${md5_command} | ${md5_command} | cut -c1-8)-${gemfile_hash} 322 | echo ${bundler_dependencies_hash} > .bundler.${project}.hash 323 | else 324 | echo "Bundler dependencies haven't changed. To force an update, delete .bundler.${project}.hash." 325 | fi 326 | fi 327 | 328 | # Install pip dependecies 329 | if [ -f requirements.txt ]; then 330 | requirements_hash=$(${md5_command} requirements.txt | cut -c1-8) 331 | pip_dependencies_hash=$(docker run --volume ${etc_volume}:/etc ${dev_image} bash -c 'find $(find /usr/local/lib/ -maxdepth 1 -name "python*" -type d | sort | tail -n 1)/dist-packages -type f -print0 | sort -z | xargs -0 '${md5_command}' | '${md5_command}' | cut -c1-8')-${requirements_hash} 332 | if [ ! -f .pip.${project}.hash ] || [ "${pip_dependencies_hash}" != "$(cat .pip.${project}.hash)" ]; then 333 | echo "Installing new pip dependencies" 334 | docker_run "" pip3 install --requirement requirements.txt 335 | pip_dependencies_hash=$(docker run --volume ${etc_volume}:/etc ${dev_image} bash -c 'find $(find /usr/local/lib/ -maxdepth 1 -name "python*" -type d | sort | tail -n 1)/dist-packages -type f -print0 | sort -z | xargs -0 '${md5_command}' | '${md5_command}' | cut -c1-8')-${requirements_hash} 336 | echo ${pip_dependencies_hash} > .pip.${project}.hash 337 | else 338 | echo "Pip dependencies haven't changed. To force an update, delete .pip.${project}.hash." 339 | fi 340 | fi 341 | } 342 | 343 | # Find current run command 344 | run_command=${1:-} 345 | if [[ -n "${run_command}" ]]; then shift; fi 346 | 347 | # Do the real business 348 | case $run_command in 349 | ""|"serve") 350 | update_dependencies 351 | 352 | # Read optional arguments 353 | detach="" 354 | forward_ports=("") 355 | run_watcher=false 356 | while [[ -n "${1:-}" ]] && [[ "${1:0:1}" == "-" ]]; do 357 | key="$1" 358 | 359 | case $key in 360 | -d|--detach) detach="--detach" ;; 361 | -p|--port) 362 | if [ -z "${2:-}" ]; then invalid "Missing port number. Usage: --port XXXX"; fi 363 | PORT=${2} 364 | shift 365 | ;; 366 | -f|--forward-port) 367 | if [ -z "${2:-}" ]; then invalid "Missing port number. Usage: --port XXXX"; fi 368 | forward_ports+=("${2}") 369 | shift 370 | ;; 371 | *) invalid "Option '${key}' not recognised." ;; 372 | esac 373 | shift 374 | done 375 | 376 | # Setup yarn dependencies 377 | if [ -f package.json ]; then 378 | run_as_user "${module_volumes}" yarn run build 379 | fi 380 | 381 | # Run watch command in the background 382 | if ${run_watcher}; then 383 | if [ -z "${detach}" ]; then trap "kill_container ${project}-watch" EXIT; fi 384 | run_as_user "--detach" yarn run watch # Run watch in the background 385 | fi 386 | 387 | publish_extra_ports="" 388 | if [ -n "${EXTRA_PORTS:-}" ]; then 389 | IFS=', ' read -r -a ports_array <<< "$EXTRA_PORTS" 390 | for extra_port in "${ports_array[@]}"; do 391 | publish_extra_ports="${publish_extra_ports} --publish $extra_port:$extra_port" 392 | done 393 | fi 394 | 395 | publish_forward_ports="" 396 | for forward_port in "${forward_ports[@]}"; do 397 | if [ -n "${forward_port}" ]; then 398 | publish_forward_ports="${publish_forward_ports} --publish ${forward_port}:${PORT}" 399 | fi 400 | done 401 | 402 | start_django_db 403 | 404 | # Run the serve container, publishing the port, and detaching if required 405 | run_as_user "--env PORT=${PORT} --publish ${PORT}:${PORT} ${publish_forward_ports} ${publish_extra_ports} ${detach} ${run_serve_docker_opts} ${module_volumes}" yarn run serve $* 406 | ;; 407 | "stop") 408 | echo "Stopping all running containers for ${project}" 409 | running_containers="$(docker ps --quiet --filter name=${project})" 410 | if [ -z "${running_containers}" ]; then 411 | echo "No running containers found" 412 | exit 0 413 | fi 414 | docker kill ${running_containers} 415 | ;; 416 | "watch") 417 | update_dependencies 418 | 419 | # Read optional arguments 420 | watch_site=false 421 | while [[ -n "${1:-}" ]] && [[ "${1:0:1}" == "-" ]]; do 422 | key="$1" 423 | 424 | case $key in 425 | -s|--watch-site) 426 | # Error if not a jekyll site 427 | if [ ! -f _config.yml ]; then 428 | echo "Error: Not a Jekyll site"; 429 | exit 1; 430 | fi 431 | watch_site=true 432 | ;; 433 | *) invalid "Option '${key}' not recognised." ;; 434 | esac 435 | shift 436 | done 437 | if ${watch_site}; then 438 | trap "kill_container ${project}-watch-site" EXIT 439 | run_as_user "--detach" jekyll build --watch # Run site watcher in the background 440 | fi 441 | run_as_user "${module_volumes}" yarn run build 442 | run_as_user "${module_volumes}" yarn run watch 443 | ;; 444 | "build") 445 | update_dependencies 446 | 447 | run_as_user "${module_volumes}" yarn run build 448 | 449 | if [ -f _config.yml ]; then 450 | # For jekyll sites 451 | run_as_user "" bundle exec jekyll build 452 | fi 453 | ;; 454 | "test") 455 | update_dependencies 456 | 457 | test_error=false 458 | 459 | # Run node tests 460 | echo "- Running yarn tests" 461 | run_as_user "" yarn run test || test_error=true 462 | 463 | # Report success or failure 464 | if ${test_error}; then 465 | echo "===" 466 | echo "Tests failed" 467 | echo "===" 468 | exit 1 469 | else 470 | echo "===" 471 | echo "Tests succeeded" 472 | echo "===" 473 | fi 474 | ;; 475 | "test-python") 476 | update_dependencies 477 | 478 | test_error=false 479 | 480 | # Run node tests 481 | echo "- Running python tests" 482 | run_as_user "" yarn run test-python || test_error=true 483 | 484 | # Report success or failure 485 | if ${test_error}; then 486 | echo "===" 487 | echo "Tests failed" 488 | echo "===" 489 | exit 1 490 | else 491 | echo "===" 492 | echo "Tests succeeded" 493 | echo "===" 494 | fi 495 | ;; 496 | "lint-python") 497 | update_dependencies 498 | 499 | lint_error=false 500 | 501 | # Run node tests 502 | echo "- Running python lint" 503 | run_as_user "" yarn run lint-python || lint_error=true 504 | 505 | # Report success or failure 506 | if ${lint_error}; then 507 | echo "===" 508 | echo "Lint failed" 509 | echo "===" 510 | exit 1 511 | else 512 | echo "===" 513 | echo "Lint succeeded" 514 | echo "===" 515 | fi 516 | ;; 517 | "clean") 518 | echo "Remove hash files" 519 | rm -rf .*.hash 520 | 521 | echo "Running 'clean' yarn script" 522 | run_as_user "" yarn run clean || true # Run the clean script 523 | 524 | echo "Removing docker objects for project: ${project}" 525 | 526 | echo "- Removing containers using project volumes" 527 | project_volumes="$(docker volume ls --quiet --filter name=${project})" 528 | for volume in ${project_volumes}; do 529 | echo " > Removing containers using volume ${volume}" 530 | containers_using_volume="$(docker ps --all --quiet --filter volume=${volume})" 531 | if [ -n "${containers_using_volume}" ]; then docker rm --force ${containers_using_volume}; fi 532 | done 533 | echo "- Removing project volumes" 534 | if [ -n "${project_volumes}" ]; then docker volume rm ${project_volumes}; fi 535 | 536 | echo "- Removing remaining project containers" 537 | project_containers="$(docker ps --all --quiet --filter name=${project})" 538 | if [ -n "${project_containers}" ]; then docker rm --force ${project_containers}; fi 539 | 540 | echo "- Removing project networks" 541 | project_networks="$(docker network ls --quiet --filter name=${project})" 542 | if [ -n "${project_networks}" ]; then docker network rm ${project_networks}; fi 543 | 544 | echo "Removing .docker-project file" 545 | rm -rf .docker-project # Remove the project file 546 | ;; 547 | "clean-cache") 548 | # Clean node cache volume 549 | echo "Removing cache volume ${cache_volume}" 550 | containers_using_volume=$(docker ps --quiet --all --filter "volume=${cache_volume}") 551 | if [ -n "${containers_using_volume}" ]; then docker rm --force ${containers_using_volume}; fi 552 | docker volume rm ${cache_volume} 553 | ;; 554 | "exec") 555 | expose_ports="" 556 | run_as_root=false 557 | 558 | while [[ -n "${1:-}" ]] && [[ "${1:0:1}" == "-" ]]; do 559 | key="$1" 560 | 561 | case $key in 562 | -r|--root) 563 | run_as_root=true 564 | ;; 565 | -p|--expose-port) 566 | if [ -z "${2:-}" ]; then invalid "Missing port number. Usage: --expose-port XXXX"; fi 567 | expose_ports="${expose_ports} --publish ${2}:${2}" 568 | shift 569 | ;; 570 | *) invalid "Option '${key}' not recognised." ;; 571 | esac 572 | shift 573 | done 574 | 575 | update_dependencies 576 | start_django_db 577 | 578 | if ${run_as_root}; then 579 | docker_run "${expose_ports}" $@ 580 | else 581 | run_as_user "${expose_ports}" $@ 582 | fi 583 | ;; 584 | "yarn") 585 | expose_ports="" 586 | run_as_root=false 587 | 588 | while [[ -n "${1:-}" ]] && [[ "${1:0:1}" == "-" ]]; do 589 | key="$1" 590 | 591 | case $key in 592 | -r|--root) 593 | run_as_root=true 594 | ;; 595 | -p|--expose-port) 596 | if [ -z "${2:-}" ]; then invalid "Missing port number. Usage: --expose-port XXXX"; fi 597 | expose_ports="${expose_ports} --publish ${2}:${2}" 598 | shift 599 | ;; 600 | *) invalid "Option '${key}' not recognised." ;; 601 | esac 602 | shift 603 | done 604 | 605 | update_dependencies 606 | start_django_db 607 | 608 | if ${run_as_root}; then 609 | docker_run "${expose_ports}" yarn run $@ 610 | else 611 | run_as_user "${expose_ports}" yarn run $@ 612 | fi 613 | ;; 614 | *) invalid "Command '${run_command}' not recognised." ;; 615 | esac 616 | -------------------------------------------------------------------------------- /permanent-redirects.yaml: -------------------------------------------------------------------------------- 1 | robots.txt?: "/static/files/robots.txt" 2 | 3 | # Documentation redirects 4 | # Home page 5 | /: https://canonical.com/anbox-cloud 6 | /contact-us: https://canonical.com/anbox-cloud#get-in-touch 7 | /thank-you: https://canonical.com/anbox-cloud#contact-form-success 8 | /terms: https://ubuntu.com/legal/terms-and-policies 9 | /privacy: https://ubuntu.com/legal/data-privacy 10 | docs/?: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/ 11 | # Tutorials 12 | docs/tutorial/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/tutorial/landing/ 13 | docs/tutorial/installing-appliance: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/tutorial/installing-appliance/ 14 | # Show by `pro enable anbox-cloud` and cannot be easily changed 15 | docs/tut/installing-appliance: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/tutorial/installing-appliance/ 16 | docs/tutorial/getting-started-dashboard: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/tutorial/getting-started-dashboard/ 17 | docs/tutorial/getting-started: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/tutorial/getting-started/ 18 | docs/tutorial/stream-client: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/tutorial/set-up-stream-client/ 19 | docs/tutorial/getting-started-aaos: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/tutorial/getting-started-aaos/ 20 | docs/tutorial/creating-addon: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/tutorial/creating-addon/ 21 | # How-to guides 22 | docs/howto/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/landing/ 23 | docs/howto/install-appliance/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/install-appliance/landing/ 24 | docs/howto/install-appliance/aws: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/install-appliance/install-on-aws/ 25 | docs/howto/install-appliance/azure: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/install-appliance/install-on-azure/ 26 | docs/howto/install-appliance/google-cloud: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/install-appliance/install-on-google-cloud/ 27 | docs/howto/install/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/install/landing/ 28 | docs/howto/install/deploy-juju: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/install/deploy-juju/ 29 | docs/howto/install/deploy-bare-metal: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/install/deploy-bare-metal/ 30 | docs/howto/install/customise: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/install/customise-installation/ 31 | docs/howto/install/high-availability: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/install/enable-high-availability/ 32 | docs/howto/install/validate: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/install/validate-deployment/ 33 | docs/howto/update/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/update/landing/ 34 | docs/howto/update/control: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/update/control-updates/ 35 | docs/howto/update/upgrade-appliance: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/update/upgrade-appliance/ 36 | docs/howto/update/upgrade-anbox: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/update/upgrade-anbox/ 37 | docs/howto/manage/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/anbox/landing/ 38 | docs/howto/manage/tls-for-appliance: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/anbox/tls-for-appliance/ 39 | docs/howto/manage/images: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/anbox/manage-images/ 40 | docs/howto/manage/ams-access: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/anbox/control-ams-remotely/ 41 | docs/howto/manage/benchmarks: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/anbox/benchmarks/ 42 | docs/howto/manage/resize-storage: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/anbox/resize-storage/ 43 | docs/howto/manage/web-dashboard: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/dashboard/landing/ 44 | docs/howto/application/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/application/landing/ 45 | docs/howto/application/create: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/application/create-application/ 46 | docs/howto/application/stream: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/application/stream-application/ 47 | docs/howto/application/wait: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/application/wait-for-application/ 48 | docs/howto/application/list: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/application/list-applications/ 49 | docs/howto/application/userdata: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/application/pass-custom-data/ 50 | docs/howto/application/test: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/application/test-application/ 51 | docs/howto/application/update: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/application/update-application/ 52 | docs/howto/application/delete: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/application/delete-application/ 53 | docs/howto/application/extend: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/application/extend-application/ 54 | docs/howto/application/virtual-devices: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/android/create-virtual-device/ 55 | docs/howto/aar/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/aar/landing/ 56 | docs/howto/aar/deploy: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/aar/deploy/ 57 | docs/howto/aar/configure: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/aar/configure/ 58 | docs/howto/aar/revoke: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/aar/revoke/ 59 | docs/howto/port/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/port/landing/ 60 | docs/howto/port/permissions: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/port/grant-runtime-permissions/ 61 | docs/howto/port/architecture: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/port/choose-apk-architecture/ 62 | docs/howto/port/obb-files: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/port/port-apk-obb-files/ 63 | docs/howto/port/configure-watchdog: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/port/configure-watchdog/ 64 | docs/howto/port/install-system-app: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/port/install-apk-system-app/ 65 | docs/howto/instance/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/instance/landing/ 66 | docs/howto/instance/create: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/instance/create-instance/ 67 | docs/howto/instance/start: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/instance/start-instance/ 68 | docs/howto/instance/wait: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/instance/wait-for-instance/ 69 | docs/howto/instance/access: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/instance/access-instance/ 70 | docs/howto/instance/list: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/instance/list-instances/ 71 | docs/howto/instance/geographic-location: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/instance/configure-geographic-location/ 72 | docs/howto/instance/logs: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/instance/view-instance-logs/ 73 | docs/howto/instance/stop: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/instance/stop-instance/ 74 | docs/howto/instance/backup-and-restore: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/instance/backup-restore-application-data/ 75 | docs/howto/instance/delete: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/instance/delete-instance/ 76 | docs/howto/instance/expose-services: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/instance/expose-services/ 77 | docs/howto/addons/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/addons/landing/ 78 | docs/howto/addons/create: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/addons/create-addon/ 79 | docs/howto/addons/enable-globally: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/addons/enable-addons-globally/ 80 | docs/howto/addons/update: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/addons/update-addon/ 81 | docs/howto/addons/migrate: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/addons/migrate-addon/ 82 | docs/howto/addons/install-tools: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/addons/install-tools-example/ 83 | docs/howto/addons/backup-and-restore: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/addons/backup-and-restore-example/ 84 | docs/howto/addons/customise-android: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/addons/customise-android-example/ 85 | docs/howto/addons/emulate-platforms: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/addons/emulate-platforms-example/ 86 | docs/howto/stream/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/stream/landing/ 87 | docs/howto/stream/access: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/stream/access-stream-gateway/ 88 | docs/howto/stream/oob-data: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/stream/exchange-oob-data/ 89 | docs/howto/stream/client-side-keyboard: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/stream/integrate-virtual-keyboard/ 90 | docs/howto/cluster/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/cluster/landing/ 91 | docs/howto/cluster/configure-nodes: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/cluster/configure-nodes/ 92 | docs/howto/cluster/scale-up: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/cluster/scale-up/ 93 | docs/howto/cluster/scale-down: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/cluster/scale-down/ 94 | docs/howto/anbox/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/anbox-runtime/landing/ 95 | docs/howto/anbox/develop-platform: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/anbox-runtime/develop-platform-plugin/ 96 | docs/howto/anbox/develop-addon: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/anbox-runtime/develop-addon-devmode/ 97 | docs/howto/android/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/android/landing/ 98 | docs/howto/android/graphics-debugging-with-renderdoc: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/android/debug-graphics-renderdoc/ 99 | docs/howto/android/custom_vhal: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/android/custom-vhal/ 100 | docs/howto/troubleshoot/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/troubleshoot/landing/ 101 | docs/howto/troubleshoot/initial-setup: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/troubleshoot/troubleshoot-initial-setup/ 102 | docs/howto/troubleshoot/logs: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/troubleshoot/view-logs/ 103 | docs/howto/troubleshoot/application-creation: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/troubleshoot/troubleshoot-application-creation/ 104 | docs/howto/troubleshoot/instance-failures: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/troubleshoot/troubleshoot-instance-failures/ 105 | docs/howto/troubleshoot/lxd-cluster: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/troubleshoot/troubleshoot-cluster-issues/ 106 | docs/howto/troubleshoot/dashboard-issues: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/troubleshoot/troubleshoot-dashboard-issues/ 107 | docs/howto/troubleshoot/streaming-issues: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/troubleshoot/troubleshoot-streaming-issues/ 108 | docs/howto/monitor/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/howto/monitor/landing/ 109 | # Reference 110 | docs/reference/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/landing/ 111 | docs/reference/releases-versions: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/release-notes/ 112 | docs/reference/roadmap: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/roadmap/ 113 | docs/reference/release-notes/release-notes: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/release-notes/ 114 | docs/reference/supported-versions: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/supported-versions/ 115 | docs/reference/component-versions: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/component-versions/ 116 | docs/reference/requirements: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/requirements/ 117 | docs/reference/appliance-command-reference/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/appliance-command-reference/landing/ 118 | docs/reference/amc-command-reference/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/landing/ 119 | docs/reference/provided-images: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/provided-images/ 120 | docs/reference/supported-rendering-resources: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/supported-rendering-resources/ 121 | docs/reference/supported-codecs: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/supported-codecs/ 122 | docs/reference/android-features: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/android-features/ 123 | docs/reference/anbox-features: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/anbox-features/ 124 | docs/reference/ams-configuration: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/ams-configuration/ 125 | docs/reference/application-manifest: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/application-manifest/ 126 | docs/reference/api-reference: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/api-reference/ 127 | docs/reference/anbox-https-api: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/anbox-https-api/ 128 | docs/reference/sdks: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/sdks/ 129 | docs/reference/network-ports: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/network-ports/ 130 | docs/reference/addon-manifest: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/addon-manifest/ 131 | docs/reference/hooks: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/hooks/ 132 | docs/reference/webrtc-streamer: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/webrtc-streamer/ 133 | docs/reference/prometheus: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/prometheus/ 134 | docs/reference/perf-benchmarks: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/perf-benchmarks/ 135 | docs/reference/deprecation-notices: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/deprecation-notices/ 136 | docs/reference/license-information: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/license-information/ 137 | docs/reference/glossary: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/glossary/ 138 | docs/reference/release-notes/1.22.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.22.2/ 139 | docs/reference/release-notes/1.22.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.22.1/ 140 | docs/reference/release-notes/1.22.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.22.0/ 141 | docs/reference/release-notes/1.21.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.21.2/ 142 | docs/reference/release-notes/1.21.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.21.1/ 143 | docs/reference/release-notes/1.21.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.21.0/ 144 | docs/reference/release-notes/1.20.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.20.2/ 145 | docs/reference/release-notes/1.20.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.20.1/ 146 | docs/reference/release-notes/1.20.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.20.0/ 147 | docs/reference/release-notes/1.19.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.19.2/ 148 | docs/reference/release-notes/1.19.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.19.1/ 149 | docs/reference/release-notes/1.19.0-fix1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.19.0-fix1/ 150 | docs/reference/release-notes/1.19.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.19.0/ 151 | docs/reference/release-notes/1.18.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.18.2/ 152 | docs/reference/release-notes/1.18.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.18.1/ 153 | docs/reference/release-notes/1.18.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.18.0/ 154 | docs/reference/release-notes/1.17.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.17.2/ 155 | docs/reference/release-notes/1.17.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.17.1/ 156 | docs/reference/release-notes/1.17.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.17.0/ 157 | docs/reference/release-notes/1.16.4: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.16.4/ 158 | docs/reference/release-notes/1.16.3: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.16.3/ 159 | docs/reference/release-notes/1.16.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.16.2/ 160 | docs/reference/release-notes/1.16.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.16.1/ 161 | docs/reference/release-notes/1.16.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.16.0/ 162 | docs/reference/release-notes/1.15.3: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.15.3/ 163 | docs/reference/release-notes/1.15.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.15.2/ 164 | docs/reference/release-notes/1.15.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.15.1/ 165 | docs/reference/release-notes/1.15.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.15.0/ 166 | docs/reference/release-notes/1.14.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.14.2/ 167 | docs/reference/release-notes/1.14.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.14.1/ 168 | docs/reference/release-notes/1.14.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.14.0/ 169 | docs/reference/release-notes/1.13.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.13.2/ 170 | docs/reference/release-notes/1.13.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.13.1/ 171 | docs/reference/release-notes/1.13.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.13.0/ 172 | docs/reference/release-notes/1.12.5: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.12.5/ 173 | docs/reference/release-notes/1.12.4: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.12.4/ 174 | docs/reference/release-notes/1.12.3: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.12.3/ 175 | docs/reference/release-notes/1.12.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.12.2/ 176 | docs/reference/release-notes/1.12.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.12.1/ 177 | docs/reference/release-notes/1.12.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.12.0/ 178 | docs/reference/release-notes/1.11.5: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.11.5/ 179 | docs/reference/release-notes/1.11.4: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.11.4/ 180 | docs/reference/release-notes/1.11.3: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.11.3/ 181 | docs/reference/release-notes/1.11.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.11.2/ 182 | docs/reference/release-notes/1.11.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.11.1/ 183 | docs/reference/release-notes/1.11.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.11.0/ 184 | docs/reference/release-notes/1.10.3: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.10.3/ 185 | docs/reference/release-notes/1.10.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.10.2/ 186 | docs/reference/release-notes/1.10.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.10.1/ 187 | docs/reference/release-notes/1.10.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.10.0/ 188 | docs/reference/release-notes/1.9.5: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.9.5/ 189 | docs/reference/release-notes/1.9.4: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.9.4/ 190 | docs/reference/release-notes/1.9.3: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.9.3/ 191 | docs/reference/release-notes/1.9.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.9.2/ 192 | docs/reference/release-notes/1.9.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.9.1/ 193 | docs/reference/release-notes/1.9.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.9.0/ 194 | docs/reference/release-notes/1.8.3: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.8.3/ 195 | docs/reference/release-notes/1.8.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.8.2/ 196 | docs/reference/release-notes/1.8.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.8.1/ 197 | docs/reference/release-notes/1.8.0: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.8.0/ 198 | docs/reference/release-notes/1.7.4: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.7.4/ 199 | docs/reference/release-notes/1.7.3: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.7.3/ 200 | docs/reference/release-notes/1.7.2: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.7.2/ 201 | docs/reference/release-notes/1.7.1: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/release-notes/1.7.1/ 202 | docs/reference/appliance-command-reference/ams: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/appliance-command-reference/ams/ 203 | docs/reference/appliance-command-reference/dashboard: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/appliance-command-reference/dashboard/ 204 | docs/reference/appliance-command-reference/destroy: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/appliance-command-reference/destroy/ 205 | docs/reference/appliance-command-reference/gateway: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/appliance-command-reference/gateway/ 206 | docs/reference/appliance-command-reference/help: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/appliance-command-reference/help/ 207 | docs/reference/appliance-command-reference/init: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/appliance-command-reference/init/ 208 | docs/reference/appliance-command-reference/status: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/appliance-command-reference/status/ 209 | docs/reference/appliance-command-reference/upgrade: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/appliance-command-reference/upgrade/ 210 | docs/reference/amc-command-reference/addon: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/addon/ 211 | docs/reference/amc-command-reference/application: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/application/ 212 | docs/reference/amc-command-reference/benchmark: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/benchmark/ 213 | docs/reference/amc-command-reference/completion: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/completion/ 214 | docs/reference/amc-command-reference/config: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/config/ 215 | docs/reference/amc-command-reference/delete: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/delete/ 216 | docs/reference/amc-command-reference/exec: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/exec/ 217 | docs/reference/amc-command-reference/help: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/help/ 218 | docs/reference/amc-command-reference/image: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/image/ 219 | docs/reference/amc-command-reference/info: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/info/ 220 | docs/reference/amc-command-reference/init: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/init/ 221 | docs/reference/amc-command-reference/launch: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/launch/ 222 | docs/reference/amc-command-reference/list: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/list/ 223 | docs/reference/amc-command-reference/logs: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/logs/ 224 | docs/reference/amc-command-reference/node: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/node/ 225 | docs/reference/amc-command-reference/remote: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/remote/ 226 | docs/reference/amc-command-reference/shell: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/shell/ 227 | docs/reference/amc-command-reference/show-log: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/show-log/ 228 | docs/reference/amc-command-reference/show: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/show/ 229 | docs/reference/amc-command-reference/start: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/start/ 230 | docs/reference/amc-command-reference/stop: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/stop/ 231 | docs/reference/amc-command-reference/wait: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/reference/cmd-ref/amc-command-reference/wait/ 232 | # Explanation 233 | docs/explanation/landing: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/landing/ 234 | docs/explanation/anbox-cloud: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/anbox-cloud/ 235 | docs/explanation/rendering-architecture: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/rendering-architecture/ 236 | docs/explanation/aaos: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/aaos/ 237 | docs/explanation/security: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/security/ 238 | docs/explanation/ams: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/ams/ 239 | docs/explanation/aar: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/aar/ 240 | docs/explanation/web-dashboard: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/web-dashboard/ 241 | docs/explanation/applications: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/applications/ 242 | docs/explanation/resources: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/resources/ 243 | docs/explanation/addons: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/addons/ 244 | docs/explanation/application-streaming: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/application-streaming/ 245 | docs/explanation/instances: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/instances/ 246 | docs/explanation/images: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/images/ 247 | docs/explanation/custom-images: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/custom-images/ 248 | docs/explanation/nodes: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/nodes/ 249 | docs/explanation/platforms: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/platforms/ 250 | docs/explanation/gpus-instances: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/gpus-instances/ 251 | docs/explanation/clustering: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/clustering/ 252 | docs/explanation/performance: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/performance/ 253 | docs/explanation/capacity-planning: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/capacity-planning/ 254 | docs/explanation/production: https://canonical-anbox-cloud-documentation.readthedocs-hosted.com/en/latest/explanation/production-planning/ 255 | -------------------------------------------------------------------------------- /static/js/anbox-stream-sdk.js: -------------------------------------------------------------------------------- 1 | // Anbox Stream SDK 2 | // Copyright 2019 Canonical Ltd. All rights reserved. 3 | 4 | class AnboxStream { 5 | /** 6 | * AnboxStream creates a connection between your client and an Android instance and 7 | * displays its video & audio feed in an HTML5 player 8 | * 9 | * @param options: {object} { 10 | * targetElement: ID of the DOM element to attach the video to. (required) 11 | * url: Address of the service. (required) 12 | * authToken: Authentication token acquired through /1.0/login (required) 13 | * stunServers: List ICE servers (default: [{"urls": ['stun:stun.l.google.com:19302'], username: "", password: ""}]) 14 | * session: { 15 | * app: Android application ID or name. (required) 16 | * }, 17 | * screen: { 18 | * width: screen width (default: 1280) 19 | * height: screen height (default: 720) 20 | * fps: screen frame rate (default: 60) 21 | * density: screen density (default: 240) 22 | * }, 23 | * controls: { 24 | * keyboard: true or false, send keypress events to the Android instance. (default: true) 25 | * mouse: true or false, send mouse and touch events to the Android instance. (default: true) 26 | * gamepad: true or false, send gamepad events to the Android instance. (default: true) 27 | * }, 28 | * callbacks: { 29 | * ready: function, called when the video and audio stream are ready to be inserted. (default: none) 30 | * error: function, called on stream error with the message as parameter. (default: none) 31 | * done: function, called when the stream is closed. (default: none) 32 | * } 33 | * } 34 | */ 35 | constructor(options) { 36 | if (this._nullOrUndef(options)) 37 | throw new Error('invalid options'); 38 | 39 | this._fillDefaults(options); 40 | this._validateOptions(options); 41 | this._options = options; 42 | 43 | this._id = Math.random().toString(36).substr(2, 9); 44 | this._containerID = options.targetElement; 45 | this._videoID = 'anbox-stream-video-' + this._id; 46 | this._audioID = 'anbox-stream-audio-' + this._id; 47 | 48 | // WebRTC 49 | this._ws = null; // WebSocket 50 | this._pc = null; // PeerConnection 51 | this._controlChan = null; // Channel to send inputs 52 | this._timedout = false; 53 | this._timer = -1; 54 | this._ready = false; 55 | 56 | // Media streams 57 | this._videoStream = null; 58 | this._audioStream = null; 59 | 60 | // Control options 61 | this._modifierState = 0; 62 | this._dimensions = null; 63 | this._gamepadManager = null; 64 | }; 65 | 66 | /** 67 | * Connect a new instance for the configured application or attach to an existing one 68 | */ 69 | connect() { 70 | // We first have to check if an instance for the application already 71 | // exists. If we already have one we attach to the existing instance. 72 | // Otherwise we create a new instance for the application. 73 | fetch(this._options.url + '/1.0/instances/', { 74 | method: 'GET', 75 | headers: { 76 | 'Accept': 'application/json, text/plain, */*', 77 | 'Authorization': 'Macaroon root=' + this._options.authToken, 78 | 'Content-Type': 'application/json', 79 | }, 80 | }) 81 | .then(response => { 82 | if (response.status !== 200) 83 | throw new Error("Failed to retrieve list of instances"); 84 | 85 | return response.json(); 86 | }) 87 | .then(jsonResp => { 88 | if (jsonResp.status !== "success") 89 | throw new Error(jsonResp.error); 90 | 91 | var instanceID = ""; 92 | for (var n = 0; n < jsonResp.metadata.length; n++) { 93 | var instance = jsonResp.metadata[n]; 94 | if (instance.application === this._options.session.app) { 95 | instanceID = instance.id; 96 | break; 97 | } 98 | } 99 | 100 | if (instanceID.length === 0) { 101 | this._createNewInstance(); 102 | return; 103 | } 104 | 105 | this._attachToInstance(instanceID); 106 | }) 107 | .catch(error => { 108 | this._options.callbacks.error(error); 109 | }); 110 | }; 111 | 112 | _attachToInstance(instanceID) { 113 | const details = { 114 | screen: { 115 | width: this._options.screen.width, 116 | height: this._options.screen.height, 117 | fps: this._options.screen.fps, 118 | density: this._options.screen.density, 119 | } 120 | } 121 | fetch(this._options.url + '/1.0/instances/' + instanceID + '/join', { 122 | method: 'POST', 123 | headers: { 124 | 'Accept': 'application/json, text/plain, */*', 125 | 'Authorization': 'Macaroon root=' + this._options.authToken, 126 | 'Content-Type': 'application/json', 127 | }, 128 | body: JSON.stringify(details), 129 | }) 130 | .then(response => { 131 | if (response.status !== 200) 132 | throw new Error("Failed to join instance"); 133 | 134 | return response.json(); 135 | }) 136 | .then(jsonResp => { 137 | if (jsonResp.status !== "success") 138 | throw new Error(jsonResp.error) 139 | 140 | // If we received any additional STUN/TURN servers from the gateway use them 141 | // If we received any additional STUN/TURN servers from the gateway use them 142 | if (!this._nullOrUndef(jsonResp.metadata.stun_servers) && jsonResp.metadata.stun_servers.length > 0) { 143 | for (var n = 0; n < jsonResp.metadata.stun_servers.length; n++) { 144 | this._options.stunServers.push({ 145 | "urls": jsonResp.metadata.stun_servers[n].urls, 146 | "username": jsonResp.metadata.stun_servers[n].username, 147 | "credential": jsonResp.metadata.stun_servers[n].password 148 | }); 149 | } 150 | } 151 | 152 | this._connectSignaler(jsonResp.metadata.websocket_url); 153 | }) 154 | .catch(error => { 155 | this._options.callbacks.error(error); 156 | }) 157 | } 158 | 159 | _createNewInstance() { 160 | const details = { 161 | name: this._options.session.app, 162 | application: this._options.session.app, 163 | } 164 | 165 | fetch(this._options.url + '/1.0/instances/', { 166 | method: 'POST', 167 | headers: { 168 | 'Accept': 'application/json, text/plain, */*', 169 | 'Authorization': 'Macaroon root=' + this._options.authToken, 170 | 'Content-Type': 'application/json', 171 | }, 172 | body: JSON.stringify(details), 173 | }) 174 | .then(response => { 175 | if (response.status !== 200) 176 | throw new Error("Failed to create new instance"); 177 | 178 | return response.json(); 179 | }) 180 | .then(jsonResp => { 181 | if (jsonResp.status !== "success") 182 | throw new Error(jsonResp.error); 183 | 184 | this._attachToInstance(jsonResp.metadata.id); 185 | }) 186 | .catch(error => { 187 | this._options.callbacks.error(error); 188 | }); 189 | } 190 | 191 | _connectSignaler(url) { 192 | let ws = new WebSocket(url); 193 | ws.onopen = this._onWsOpen.bind(this); 194 | ws.onclose = this._onWsClose.bind(this); 195 | ws.onerror = this._onWsError.bind(this); 196 | ws.onmessage = this._onWsMessage.bind(this); 197 | 198 | this._ws = ws; 199 | this._timer = window.setTimeout(this._onTimeout.bind(this), 2 * 60 * 1000); 200 | } 201 | 202 | /** 203 | * Disconnect an existing stream and remove the video & audio elements. 204 | * 205 | * This will stop the underlying Android instance. 206 | */ 207 | disconnect() { 208 | this._stopStreaming(); 209 | }; 210 | 211 | /** 212 | * Toggle fullscreen for the streamed video. 213 | * 214 | * IMPORTANT: fullscreen can only be toggled following a user input. 215 | * If you call this method when your page loads, it will not work. 216 | */ 217 | requestFullscreen() { 218 | if (!document.fullscreenEnabled) { 219 | console.error("fullscreen not supported"); 220 | } else { 221 | const video = document.getElementById(this._videoID); 222 | if (video.requestFullscreen) { 223 | video.requestFullscreen(); 224 | } else if (video.mozRequestFullScreen) { /* Firefox */ 225 | video.mozRequestFullScreen(); 226 | } else if (video.webkitRequestFullscreen) { /* Chrome, Safari and Opera */ 227 | video.webkitRequestFullscreen(); 228 | } else if (video.msRequestFullscreen) { /* IE/Edge */ 229 | video.msRequestFullscreen(); 230 | } 231 | } 232 | }; 233 | 234 | /** 235 | * Exit fullscreen mode. 236 | */ 237 | exitFullscreen() { 238 | document.exitFullscreen(); 239 | }; 240 | 241 | /** 242 | * Return the stream ID you can use to access video and audio elements with getElementById 243 | */ 244 | getId() { 245 | return this._id; 246 | } 247 | 248 | _fillDefaults(options) { 249 | if (this._nullOrUndef(options.screen)) 250 | options.screen = {}; 251 | 252 | if (this._nullOrUndef(options.screen.width)) 253 | options.screen.width = 1280; 254 | 255 | if (this._nullOrUndef(options.screen.height)) 256 | options.screen.height = 720; 257 | 258 | if (this._nullOrUndef(options.screen.fps)) 259 | options.screen.fps = 60; 260 | 261 | if (this._nullOrUndef(options.screen.density)) 262 | options.screen.density = 240; 263 | 264 | if (this._nullOrUndef(options.controls)) 265 | options.controls = {}; 266 | 267 | if (this._nullOrUndef(options.controls.key)) 268 | options.controls.keyboard = true; 269 | 270 | if (this._nullOrUndef(options.controls.mouse)) 271 | options.controls.mouse = true; 272 | 273 | if (this._nullOrUndef(options.controls.gamepad)) 274 | options.controls.gamepad = true; 275 | 276 | if (this._nullOrUndef(options.stunServers)) 277 | options.stunServers = [{ urls: ['stun:stun.l.google.com:19302'], username: "", password: ""}]; 278 | 279 | if (this._nullOrUndef(options.callbacks)) 280 | options.callbacks = {}; 281 | 282 | if (this._nullOrUndef(options.callbacks.ready)) 283 | options.callbacks.ready = () => {}; 284 | 285 | if (this._nullOrUndef(options.callbacks.error)) 286 | options.callbacks.error = () => {}; 287 | 288 | if (this._nullOrUndef(options.callbacks.done)) 289 | options.callbacks.done = () => {}; 290 | }; 291 | 292 | _validateOptions(options) { 293 | // Required 294 | if (this._nullOrUndef(options.targetElement)) 295 | throw new Error('missing targetElement parameter'); 296 | if (document.getElementById(options.targetElement) === null) { 297 | throw new Error(`target element "${options.targetElement}" does not exist`) 298 | } 299 | 300 | if (this._nullOrUndef(options.authToken)) 301 | throw new Error('missing authToken parameter'); 302 | 303 | if (this._nullOrUndef(options.session)) 304 | throw new Error('missing session parameter'); 305 | 306 | if (this._nullOrUndef(options.session.app)) 307 | throw new Error('missing session.app parameter'); 308 | 309 | if (this._nullOrUndef(options.url)) 310 | throw new Error('missing url parameter'); 311 | 312 | if (!options.url.includes('https') && !options.url.includes('http')) 313 | throw new Error('unsupported scheme'); 314 | } 315 | 316 | _insertMedia(videoSource, audioSource) { 317 | this._ready = true; 318 | let mediaContainer = document.getElementById(this._containerID); 319 | 320 | const video = document.createElement('video'); 321 | video.srcObject = videoSource; 322 | video.muted = true; 323 | video.autoplay = true; 324 | video.controls = false; 325 | video.id = this._videoID; 326 | 327 | const audio = document.createElement('audio'); 328 | audio.id = this._audioID; 329 | audio.srcObject = audioSource; 330 | audio.autoplay = true; 331 | audio.controls = false; 332 | 333 | mediaContainer.appendChild(video); 334 | mediaContainer.appendChild(audio); 335 | 336 | this._registerControls() 337 | }; 338 | 339 | _removeMedia() { 340 | const video = document.getElementById(this._videoID); 341 | const audio = document.getElementById(this._audioID); 342 | 343 | if (video) 344 | video.remove(); 345 | if (audio) 346 | audio.remove(); 347 | }; 348 | 349 | _stopStreaming() { 350 | if (this._pc !== null) { 351 | this._pc.close(); 352 | this._pc = null; 353 | } 354 | if (this._ws !== null) { 355 | this._ws.close(); 356 | this._ws = null; 357 | } 358 | this._removeMedia(); 359 | this._unregisterControls(); 360 | 361 | if (this._gamepadManager) { 362 | this._gamepadManager.stopPolling() 363 | } 364 | this._options.callbacks.done() 365 | }; 366 | 367 | _onTimeout() { 368 | if (this._pc == null || this._pc.iceConnectionState === 'connected') 369 | return; 370 | 371 | this._timedout = true; 372 | this._stopStreaming(); 373 | }; 374 | 375 | _onRtcOfferCreated(description) { 376 | this._pc.setLocalDescription(description); 377 | let msg = {type: 'offer', sdp: btoa(description.sdp)}; 378 | if (this._ws.readyState === 1) 379 | this._ws.send(JSON.stringify(msg)) 380 | }; 381 | 382 | _onRtcTrack(event) { 383 | const kind = event.track.kind; 384 | if (kind === 'video') { 385 | this._videoStream = event.streams[0]; 386 | this._videoStream.onremovetrack = this._stopStreaming; 387 | } else if (kind === 'audio') { 388 | this._audioStream = event.streams[0]; 389 | this._audioStream.onremovetrack = this._stopStreaming; 390 | } 391 | 392 | // Start streaming until audio and video tracks both are available 393 | if (this._videoStream && this._audioStream) { 394 | this._insertMedia(this._videoStream, this._audioStream) 395 | this._options.callbacks.ready(); 396 | } 397 | }; 398 | 399 | _onRtcIceConnectionStateChange() { 400 | if (this._pc === null) 401 | return; 402 | 403 | if (this._pc.iceConnectionState === 'failed') { 404 | this._stopStreaming(); 405 | this._options.callbacks.error(new Error('Failed to establish a connection via ICE')); 406 | } else if (this._pc.iceConnectionState === 'disconnected' || 407 | this._pc.iceConnectionState === 'closed') { 408 | if (this._timedout) { 409 | this._options.callbacks.error(new Error('Connection timed out')); 410 | return; 411 | } 412 | this._options.callbacks.error(new Error('Connection lost')); 413 | this._stopStreaming(); 414 | } else if (this._pc.iceConnectionState === 'connected') { 415 | window.clearTimeout(this._timer); 416 | this._ws.close(); 417 | } 418 | }; 419 | 420 | _onRtcIceCandidate(event) { 421 | if (event.candidate !== null && event.candidate.candidate !== "") { 422 | const msg = { 423 | type: 'candidate', 424 | candidate: btoa(event.candidate.candidate), 425 | sdpMid: event.candidate.sdpMid, 426 | sdpMLineIndex: event.candidate.sdpMLineIndex, 427 | }; 428 | if (this._ws.readyState === 1) 429 | this._ws.send(JSON.stringify(msg)); 430 | } 431 | }; 432 | 433 | _registerControls() { 434 | const v = document.getElementById(this._videoID); 435 | 436 | if (this._options.controls.mouse) { 437 | if (window.matchMedia('(pointer:fine)')) { 438 | v.addEventListener('mousemove', this._onMouseMove.bind(this)); 439 | v.addEventListener('mousedown', this._onMouseButton.bind(this)); 440 | v.addEventListener('mouseup', this._onMouseButton.bind(this)); 441 | v.addEventListener('touchstart', this._onTouchStart.bind(this)); 442 | v.addEventListener('touchend', this._onTouchEnd.bind(this)); 443 | v.addEventListener('touchcancel', this._onTouchCancel.bind(this)); 444 | v.addEventListener('touchmove', this._onTouchMove.bind(this)); 445 | v.addEventListener('resize', this._refreshWindowMath.bind(this)); 446 | } else 447 | console.warn("Device does not have mouse support") 448 | } 449 | 450 | if (this._options.controls.keyboard) { 451 | window.addEventListener('keydown', this._onKey.bind(this)); 452 | window.addEventListener('keyup', this._onKey.bind(this)); 453 | window.addEventListener('gamepadconnected', this._queryGamePadEvents.bind(this)); 454 | } 455 | 456 | if (this._options.controls.keyboard || this._options.controls.mouse) { 457 | // Call it once for the initial values and refresh it every time the window 458 | // or video element is resized 459 | this._refreshWindowMath(); 460 | window.addEventListener('resize', this._refreshWindowMath.bind(this)); 461 | } 462 | }; 463 | 464 | _unregisterControls() { 465 | const v = document.getElementById(this._videoID); 466 | 467 | // Removing the video container should automatically remove all event listeners 468 | // but this is dependant on the garbage collector, so we manually do it if we can 469 | if (v) { 470 | v.removeEventListener('mousemove', this._onMouseMove); 471 | v.removeEventListener('mousedown', this._onMouseButton); 472 | v.removeEventListener('mouseup', this._onMouseButton); 473 | v.removeEventListener('touchstart', this._onTouchStart); 474 | v.removeEventListener('touchend', this._onTouchEnd); 475 | v.removeEventListener('touchcancel', this._onTouchCancel); 476 | v.removeEventListener('touchmove', this._onTouchMove); 477 | v.removeEventListener('resize', this._refreshWindowMath); 478 | } 479 | 480 | window.removeEventListener('resize', this._refreshWindowMath.bind(this)); 481 | window.removeEventListener('keydown', this._onKey.bind(this)); 482 | window.removeEventListener('keyup', this._onKey.bind(this)); 483 | window.removeEventListener('gamepadconnected', this._queryGamePadEvents.bind(this)); 484 | }; 485 | 486 | _clientToServerX(clientX, d) { 487 | let serverX = Math.round((clientX - d.containerOffsetX) * d.scalingFactorX); 488 | if (serverX === d.frameW - 1) serverX = d.frameW; 489 | if (serverX > d.frameW) serverX = d.frameW; 490 | if (serverX < 0) serverX = 0; 491 | return serverX; 492 | }; 493 | 494 | _clientToServerY(clientY, m) { 495 | let serverY = Math.round((clientY - m.containerOffsetY) * m.scalingFactorY); 496 | if (serverY === m.frameH - 1) serverY = m.frameH; 497 | if (serverY > m.frameH) serverY = m.frameH; 498 | if (serverY < 0) serverY = 0; 499 | return serverY; 500 | }; 501 | 502 | _triggerModifierEvent(event, key) { 503 | if (event.getModifierState(key)) { 504 | if (!(this._modifierState & _modifierEnum[key])) { 505 | this._modifierState = this._modifierState | _modifierEnum[key]; 506 | this._sendEvent('key', {code: _keyScancodes[key], pressed: true}); 507 | } 508 | } else { 509 | if ((this._modifierState & _modifierEnum[key])) { 510 | this._modifierState = this._modifierState & ~_modifierEnum[key]; 511 | this._sendEvent('key', {code: _keyScancodes[key], pressed: false}); 512 | } 513 | } 514 | }; 515 | 516 | _sendEvent(type, data) { 517 | if (this._pc === null || this._controlChan.readyState !== 'open') 518 | return; 519 | this._controlChan.send(JSON.stringify({type: 'input::' + type, data: data})); 520 | }; 521 | 522 | _refreshWindowMath() { 523 | let video = document.getElementById(this._videoID); 524 | 525 | // timing issues can occur when removing the component 526 | if (!video) { 527 | return 528 | } 529 | 530 | const windowW = video.offsetWidth; 531 | const windowH = video.offsetHeight; 532 | const frameW = video.videoWidth; 533 | const frameH = video.videoHeight; 534 | 535 | const multi = Math.min(windowW / frameW, windowH / frameH); 536 | const vpWidth = frameW * multi; 537 | const vpHeight = frameH * multi; 538 | 539 | this._dimensions = { 540 | scalingFactorX: frameW / vpWidth, 541 | scalingFactorY: frameH / vpHeight, 542 | containerOffsetX: Math.max((windowW - vpWidth) / 2.0, 0), 543 | containerOffsetY: Math.max((windowH - vpHeight) / 2.0, 0), 544 | frameW, 545 | frameH, 546 | }; 547 | }; 548 | 549 | _onMouseMove(event) { 550 | const x = this._clientToServerX(event.offsetX, this._dimensions); 551 | const y = this._clientToServerY(event.offsetY, this._dimensions); 552 | this._sendEvent('mouse-move', {x: x, y: y, rx: event.movementX, ry: event.movementY}) 553 | }; 554 | 555 | _onMouseButton(event) { 556 | const down = event.type === 'mousedown'; 557 | let button; 558 | 559 | if (down && event.button === 0 && event.ctrlKey && event.shiftKey) 560 | return; 561 | 562 | switch (event.button) { 563 | case 0: button = 1; break; 564 | case 1: button = 2; break; 565 | case 2: button = 3; break; 566 | case 3: button = 4; break; 567 | case 4: button = 5; break; 568 | default: break; 569 | } 570 | 571 | this._sendEvent('mouse-button', {button: button, pressed: down}) 572 | }; 573 | 574 | _onKey(event) { 575 | // Disable any problematic browser shortcuts 576 | if (event.code === 'F5' || // Reload 577 | (event.code === 'KeyR' && event.ctrlKey) || // Reload 578 | (event.code === 'F5' && event.ctrlKey) || // Hard reload 579 | (event.code === 'KeyI' && event.ctrlKey && event.shiftKey) || 580 | (event.code === 'F11') || // Fullscreen 581 | (event.code === 'F12') // Developer tools 582 | ) return; 583 | 584 | event.preventDefault(); 585 | 586 | const code = _keyScancodes[event.code]; 587 | const pressed = (event.type === 'keydown'); 588 | if (code) { 589 | // NOTE: no need to check the following modifier keys 590 | // 'ScrollLock', 'NumLock', 'CapsLock' 591 | // as they're mapped to event.code correctly 592 | const modifierKeys = ['Control', 'Shift', 'Alt', 'Meta', 'AltGraph']; 593 | for (let i = 0; i < modifierKeys.length; i++) { 594 | this._triggerModifierEvent(event, modifierKeys[i]); 595 | } 596 | 597 | this._sendEvent('key', {code: code, pressed: pressed}); 598 | } 599 | }; 600 | 601 | _touchEvent(event, eventType) { 602 | event.preventDefault(); 603 | for (let n = 0; n < event.changedTouches.length; n++) { 604 | let touch = event.changedTouches[n]; 605 | let x = this._clientToServerX(touch.clientX, this._dimensions); 606 | let y = this._clientToServerY(touch.clientY, this._dimensions); 607 | this._sendEvent(eventType, {id: n, x: x, y: y}); 608 | } 609 | }; 610 | 611 | _onTouchStart(event) {this._touchEvent(event, 'touch-start')}; 612 | _onTouchEnd(event) {this._touchEvent(event, 'touch-end')}; 613 | _onTouchCancel(event) {this._touchEvent(event, 'touch-cancel')}; 614 | _onTouchMove(event) {this._touchEvent(event, 'touch-move')}; 615 | 616 | _queryGamePadEvents() { 617 | if (!this._options.controls.gamepad) 618 | return; 619 | let gamepads = navigator.getGamepads(); 620 | if (gamepads.length > 0) { 621 | this._gamepadManager = new _gamepadEventManager(this._sendEvent.bind(this)); 622 | this._gamepadManager.startPolling() 623 | } 624 | }; 625 | 626 | _nullOrUndef(obj) { return obj === null || obj === undefined }; 627 | 628 | _onWsOpen() { 629 | const config = { iceServers: this._options.stunServers }; 630 | this._pc = new RTCPeerConnection(config); 631 | this._pc.ontrack = this._onRtcTrack.bind(this); 632 | this._pc.oniceconnectionstatechange = this._onRtcIceConnectionStateChange.bind(this); 633 | this._pc.onicecandidate = this._onRtcIceCandidate.bind(this); 634 | 635 | this._controlChan = this._pc.createDataChannel('control'); 636 | let options = {offerToReceiveVideo: true, offerToReceiveAudio: true}; 637 | this._pc.createOffer(options).then(this._onRtcOfferCreated.bind(this)).catch(function(err) { 638 | console.error(err) 639 | }); 640 | }; 641 | 642 | _onWsClose() { 643 | if (!this._ready) { 644 | this._options.callbacks.error(new Error('Connection was interrupted while connecting')); 645 | } 646 | }; 647 | 648 | _onWsError(event) { 649 | if (event.type === 'error') { 650 | this._stopStreaming(); 651 | } 652 | this._options.callbacks.error(new Error('failed to communicate with backend service')); 653 | }; 654 | 655 | _onWsMessage(event) { 656 | const msg = JSON.parse(event.data); 657 | if (msg.type === 'answer') { 658 | this._pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: atob(msg.sdp)})); 659 | } else if (msg.type === 'candidate') { 660 | this._pc.addIceCandidate({'candidate': atob(msg.candidate), 'sdpMLineIndex': msg.sdpMLineIndex, 'sdpMid': msg.sdpMid}) 661 | } else { 662 | console.log('Unknown message type ' + msg.type) 663 | } 664 | }; 665 | } 666 | 667 | 668 | class _gamepadEventManager { 669 | constructor(sendEvent) { 670 | this._polling = false; 671 | this._state = {}; 672 | this._dpad_remap_start_index = 6; 673 | this._dpad_standard_start_index = 12; 674 | this._sendEvent = sendEvent 675 | } 676 | 677 | startPolling() { 678 | if (this._polling === true) 679 | return; 680 | 681 | // Since chrome only supports event polling and we don't want 682 | // to send any gamepad events to Android isntance if the state 683 | // of any button or axis of gamepad is not changed. Hence we 684 | // cache all keys state whenever it gets connected and provide 685 | // event-driven gamepad events mechanism for gamepad events processing. 686 | let gamepads = navigator.getGamepads(); 687 | for (let i = 0; i < gamepads.length; i++) { 688 | if (gamepads[i]) 689 | this.cacheState(gamepads[i]); 690 | } 691 | 692 | this._polling = true; 693 | this.tick() 694 | }; 695 | 696 | stopPolling() { 697 | if (this._polling === true) 698 | this._polling = false; 699 | }; 700 | 701 | tick() { 702 | this.queryEvents(); 703 | if (this._polling) 704 | window.requestAnimationFrame(this.tick.bind(this)); 705 | }; 706 | 707 | queryEvents() { 708 | let gamepads = navigator.getGamepads(); 709 | for (let i = 0; i < gamepads.length; i++) { 710 | let gamepad = gamepads[i]; 711 | if (gamepad) { 712 | // A new gamepad is added 713 | if (!this._state[gamepad]) 714 | this.cacheState(gamepad); 715 | else { 716 | const buttons = gamepad.buttons; 717 | const cacheButtons = this._state[gamepad].buttons; 718 | for (let j = 0; j < buttons.length; j++) { 719 | if (cacheButtons[j].pressed !== buttons[j].pressed) { 720 | // Check the table at the following link that describes the buttons/axes 721 | // index and their physical locations. 722 | this._sendEvent('gamepad-button', {id: gamepad.index, index: j, pressed: buttons[j].pressed}); 723 | cacheButtons[j].pressed = buttons[j].pressed; 724 | } 725 | } 726 | 727 | // NOTE: For some game controllers, E.g. PS3 or Xbox 360 controller, DPAD buttons 728 | // were translated to axes via html5 gamepad APIs and located in gamepad.axes array 729 | // indexed starting from 6 to 7. 730 | // When a DPAD button is pressed/unpressed, the corresponding value as follows 731 | // 732 | // Button | Index | Pressed | Unpressed | 733 | // DPAD_LEFT_BUTTON | 6 | -1 | 0 | 734 | // DPAD_RIGHT_BUTTON | 6 | 1 | 0 | 735 | // DPAD_UP_BUTTON | 7 | -1 | 0 | 736 | // DPAD_DOWN_BUTTON | 7 | 1 | 0 | 737 | // 738 | // When the above button was pressed/unpressed, we will send the gamepad-button 739 | // event instead. 740 | const axes = gamepad.axes; 741 | let dpad_button_index = 0; 742 | const cacheAxes = this._state[gamepad].axes; 743 | for (let k = 0; k < axes.length; k++) { 744 | if (cacheAxes[k] !== axes[k]) { 745 | switch (true) { 746 | case k < this._dpad_remap_start_index: // Standard axes 747 | this._sendEvent('gamepad-axes', {id: gamepad.index, index: k, value: axes[k]}); 748 | break; 749 | case k === this._dpad_remap_start_index: // DPAD left and right buttons 750 | if (axes[k] === 0) {} 751 | else if (axes[k] === -1) { 752 | dpad_button_index = this._dpad_standard_start_index + 2; 753 | } else { 754 | dpad_button_index = this._dpad_standard_start_index + 3; 755 | } 756 | 757 | this._sendEvent('gamepad-button', { 758 | id: gamepad.index, 759 | index: dpad_button_index, 760 | pressed: axes[k] !== 0 761 | }); 762 | break; 763 | case k === this._dpad_remap_start_index + 1: // DPAD up and down buttons 764 | if (axes[k] === 0) {} 765 | else if (axes[k] === -1) { 766 | dpad_button_index = this._dpad_standard_start_index; 767 | } else { 768 | dpad_button_index = this._dpad_standard_start_index + 1; 769 | } 770 | 771 | this._sendEvent('gamepad-button', { 772 | id: gamepad.index, 773 | index: dpad_button_index, 774 | pressed: axes[k] !== 0 775 | }); 776 | break; 777 | default: 778 | console.log("Unsupported axes index", k); 779 | break; 780 | } 781 | cacheAxes[k] = axes[k] 782 | } 783 | } 784 | } 785 | } 786 | } 787 | }; 788 | 789 | cacheState(gamepad) { 790 | if (!gamepad) 791 | return; 792 | 793 | const gamepadState = {}; 794 | const buttons = gamepad.buttons; 795 | for (let index = 0; index < buttons.length; index++) { 796 | let buttonState = { 797 | pressed: buttons[index].pressed 798 | }; 799 | if (gamepadState.buttons) 800 | gamepadState.buttons.push(buttonState); 801 | else 802 | gamepadState.buttons = [buttonState]; 803 | } 804 | 805 | const axes = gamepad.axes; 806 | for (let index = 0; index < axes.length; index++) { 807 | if (gamepadState.axes) 808 | gamepadState.axes.push(axes[index]); 809 | else 810 | gamepadState.axes = [axes[index]]; 811 | } 812 | 813 | this._state[gamepad] = gamepadState; 814 | } 815 | } 816 | 817 | const _keyScancodes = { 818 | KeyA: 4, 819 | KeyB: 5, 820 | KeyC: 6, 821 | KeyD: 7, 822 | KeyE: 8, 823 | KeyF: 9, 824 | KeyG: 10, 825 | KeyH: 11, 826 | KeyI: 12, 827 | KeyJ: 13, 828 | KeyK: 14, 829 | KeyL: 15, 830 | KeyM: 16, 831 | KeyN: 17, 832 | KeyO: 18, 833 | KeyP: 19, 834 | KeyQ: 20, 835 | KeyR: 21, 836 | KeyS: 22, 837 | KeyT: 23, 838 | KeyU: 24, 839 | KeyV: 25, 840 | KeyW: 26, 841 | KeyX: 27, 842 | KeyY: 28, 843 | KeyZ: 29, 844 | Digit1: 30, 845 | Digit2: 31, 846 | Digit3: 32, 847 | Digit4: 33, 848 | Digit5: 34, 849 | Digit6: 35, 850 | Digit7: 36, 851 | Digit8: 37, 852 | Digit9: 38, 853 | Digit0: 39, 854 | Enter: 40, 855 | Escape: 41, 856 | Backspace: 42, 857 | Tab: 43, 858 | Space: 44, 859 | Minus: 45, 860 | Equal: 46, 861 | BracketLeft: 47, 862 | BracketRight: 48, 863 | Backslash: 49, 864 | Semicolon: 51, 865 | Comma: 54, 866 | Period: 55, 867 | Slash: 56, 868 | CapsLock: 57, 869 | F1: 58, 870 | F2: 59, 871 | F3: 60, 872 | F4: 61, 873 | F5: 62, 874 | F6: 63, 875 | F7: 64, 876 | F8: 65, 877 | F9: 66, 878 | F10: 67, 879 | F11: 68, 880 | F12: 69, 881 | PrintScreen: 70, 882 | ScrollLock: 71, 883 | Pause: 72, 884 | Insert: 73, 885 | Home: 74, 886 | PageUp: 75, 887 | Delete: 76, 888 | End: 77, 889 | PageDown: 78, 890 | ArrowRight: 79, 891 | ArrowLeft: 80, 892 | ArrowDown: 81, 893 | ArrowUp: 82, 894 | Control: 83, 895 | Shift: 84, 896 | Alt: 85, 897 | Meta: 86, 898 | AltGraph: 87, 899 | NumLock: 88, 900 | }; 901 | 902 | const _modifierEnum = { 903 | Control: 0x1, 904 | Shift: 0x2, 905 | Alt: 0x4, 906 | Meta: 0x8, 907 | AltGraph: 0x10, 908 | }; 909 | 910 | export default AnboxStream; 911 | --------------------------------------------------------------------------------