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