• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2022 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15
16"""Interface for controller-specific Pandora server management."""
17
18import asyncio
19import avatar.aio
20import grpc
21import grpc.aio
22import threading
23import types
24
25from avatar.bumble_device import BumbleDevice
26from avatar.bumble_server import serve_bumble
27from avatar.controllers import bumble_device, pandora_device
28from avatar.pandora_client import BumblePandoraClient, PandoraClient
29from contextlib import suppress
30from mobly.controllers import android_device
31from mobly.controllers.android_device import AndroidDevice
32from typing import Generic, Optional, TypeVar
33
34ANDROID_SERVER_PACKAGE = 'com.android.pandora'
35ANDROID_SERVER_GRPC_PORT = 8999  # TODO: Use a dynamic port
36
37
38# Generic type for `PandoraServer`.
39TDevice = TypeVar('TDevice')
40
41
42class PandoraServer(Generic[TDevice]):
43    """Abstract interface to manage the Pandora gRPC server on the device."""
44
45    MOBLY_CONTROLLER_MODULE: types.ModuleType = pandora_device
46
47    device: TDevice
48
49    def __init__(self, device: TDevice) -> None:
50        """Creates a PandoraServer.
51
52        Args:
53            device: A Mobly controller instance.
54        """
55        self.device = device
56
57    def start(self) -> PandoraClient:
58        """Sets up and starts the Pandora server on the device."""
59        assert isinstance(self.device, PandoraClient)
60        return self.device
61
62    def stop(self) -> None:
63        """Stops and cleans up the Pandora server on the device."""
64
65
66class BumblePandoraServer(PandoraServer[BumbleDevice]):
67    """Manages the Pandora gRPC server on a BumbleDevice."""
68
69    MOBLY_CONTROLLER_MODULE = bumble_device
70
71    _task: Optional[asyncio.Task[None]] = None
72
73    def start(self) -> BumblePandoraClient:
74        """Sets up and starts the Pandora server on the Bumble device."""
75        assert self._task is None
76
77        # set the event loop to make sure the gRPC server use the avatar one.
78        asyncio.set_event_loop(avatar.aio.loop)
79
80        # create gRPC server & port.
81        server = grpc.aio.server()
82        port = server.add_insecure_port(f'localhost:{0}')
83
84        self._task = avatar.aio.loop.create_task(serve_bumble(self.device, grpc_server=server, port=port))
85
86        return BumblePandoraClient(f'localhost:{port}', self.device)
87
88    def stop(self) -> None:
89        """Stops and cleans up the Pandora server on the Bumble device."""
90
91        async def server_stop() -> None:
92            assert self._task is not None
93            if not self._task.done():
94                self._task.cancel()
95                with suppress(asyncio.CancelledError):
96                    await self._task
97            self._task = None
98
99        avatar.aio.run_until_complete(server_stop())
100
101
102class AndroidPandoraServer(PandoraServer[AndroidDevice]):
103    """Manages the Pandora gRPC server on an AndroidDevice."""
104
105    MOBLY_CONTROLLER_MODULE = android_device
106
107    _instrumentation: Optional[threading.Thread] = None
108    _port: int = ANDROID_SERVER_GRPC_PORT
109
110    def start(self) -> PandoraClient:
111        """Sets up and starts the Pandora server on the Android device."""
112        assert self._instrumentation is None
113
114        # start Pandora Android gRPC server.
115        self._instrumentation = threading.Thread(
116            target=lambda: self.device.adb._exec_adb_cmd(  # type: ignore
117                'shell',
118                f'am instrument --no-hidden-api-checks -w {ANDROID_SERVER_PACKAGE}/.Main',
119                shell=False,
120                timeout=None,
121                stderr=None,
122            )
123        )
124
125        self._instrumentation.start()
126        self.device.adb.forward([f'tcp:{self._port}', f'tcp:{ANDROID_SERVER_GRPC_PORT}'])  # type: ignore
127
128        return PandoraClient(f'localhost:{self._port}', 'android')
129
130    def stop(self) -> None:
131        """Stops and cleans up the Pandora server on the Android device."""
132        assert self._instrumentation is not None
133
134        # Stop Pandora Android gRPC server.
135        self.device.adb._exec_adb_cmd(  # type: ignore
136            'shell', f'am force-stop {ANDROID_SERVER_PACKAGE}', shell=False, timeout=None, stderr=None
137        )
138
139        self.device.adb.forward(['--remove', f'tcp:{ANDROID_SERVER_GRPC_PORT}'])  # type: ignore
140        self._instrumentation.join()
141        self._instrumentation = None
142