1# Copyright 2016 Google Inc. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""JSON RPC interface to android scripting engine.""" 15 16import time 17 18from mobly import utils 19from mobly.controllers.android_device_lib import event_dispatcher 20from mobly.controllers.android_device_lib import jsonrpc_client_base 21 22_APP_NAME = 'SL4A' 23_DEVICE_SIDE_PORT = 8080 24_LAUNCH_CMD = ( 25 'am start -a com.googlecode.android_scripting.action.LAUNCH_SERVER ' 26 '--ei com.googlecode.android_scripting.extra.USE_SERVICE_PORT %s ' 27 'com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher' 28) 29# Maximum time to wait for the app to start on the device (10 minutes). 30# TODO: This timeout is set high in order to allow for retries in 31# start_app_and_connect. Decrease it when the call to connect() has the option 32# for a quicker timeout than the default _cmd() timeout. 33# TODO: Evaluate whether the high timeout still makes sense for sl4a. It was 34# designed for user snippets which could be very slow to start depending on the 35# size of the snippet and main apps. sl4a can probably use a much smaller value. 36_APP_START_WAIT_TIME = 2 * 60 37 38 39class Sl4aClient(jsonrpc_client_base.JsonRpcClientBase): 40 """A client for interacting with SL4A using Mobly Snippet Lib. 41 42 Extra public attributes: 43 ed: Event dispatcher instance for this sl4a client. 44 """ 45 46 def __init__(self, ad): 47 """Initializes an Sl4aClient. 48 49 Args: 50 ad: AndroidDevice object. 51 """ 52 super().__init__(app_name=_APP_NAME, ad=ad) 53 self._ad = ad 54 self.ed = None 55 self._adb = ad.adb 56 57 def start_app_and_connect(self): 58 """Overrides superclass.""" 59 # Check that sl4a is installed 60 out = self._adb.shell('pm list package') 61 if not utils.grep('com.googlecode.android_scripting', out): 62 raise jsonrpc_client_base.AppStartError( 63 self._ad, '%s is not installed on %s' % (_APP_NAME, self._adb.serial) 64 ) 65 self.disable_hidden_api_blacklist() 66 67 # sl4a has problems connecting after disconnection, so kill the apk and 68 # try connecting again. 69 try: 70 self.stop_app() 71 except Exception as e: 72 self.log.warning(e) 73 74 # Launch the app 75 self.device_port = _DEVICE_SIDE_PORT 76 self._adb.shell(_LAUNCH_CMD % self.device_port) 77 78 # Try to start the connection (not restore the connectivity). 79 # The function name restore_app_connection is used here is for the 80 # purpose of reusing the same code as it does when restoring the 81 # connection. And we do not want to come up with another function 82 # name to complicate the API. Change the name if necessary. 83 self.restore_app_connection() 84 85 def restore_app_connection(self, port=None): 86 """Restores the sl4a after device got disconnected. 87 88 Instead of creating new instance of the client: 89 - Uses the given port (or find a new available host_port if none is 90 given). 91 - Tries to connect to remote server with selected port. 92 93 Args: 94 port: If given, this is the host port from which to connect to remote 95 device port. If not provided, find a new available port as host 96 port. 97 98 Raises: 99 AppRestoreConnectionError: When the app was not able to be started. 100 """ 101 self.host_port = port or utils.get_available_host_port() 102 self._retry_connect() 103 self.ed = self._start_event_client() 104 105 def stop_app(self): 106 """Overrides superclass.""" 107 try: 108 if self._conn: 109 # Be polite; let the dest know we're shutting down. 110 try: 111 self.closeSl4aSession() 112 except Exception: 113 self.log.exception( 114 'Failed to gracefully shut down %s.', self.app_name 115 ) 116 117 # Close the socket connection. 118 self.disconnect() 119 self.stop_event_dispatcher() 120 121 # Terminate the app 122 self._adb.shell('am force-stop com.googlecode.android_scripting') 123 finally: 124 # Always clean up the adb port 125 self.clear_host_port() 126 127 def stop_event_dispatcher(self): 128 # Close Event Dispatcher 129 if self.ed: 130 try: 131 self.ed.clean_up() 132 except Exception: 133 self.log.exception('Failed to shutdown sl4a event dispatcher.') 134 self.ed = None 135 136 def _retry_connect(self): 137 self._adb.forward(['tcp:%d' % self.host_port, 'tcp:%d' % self.device_port]) 138 expiration_time = time.perf_counter() + _APP_START_WAIT_TIME 139 started = False 140 while time.perf_counter() < expiration_time: 141 self.log.debug('Attempting to start %s.', self.app_name) 142 try: 143 self.connect() 144 started = True 145 break 146 except Exception: 147 self.log.debug( 148 '%s is not yet running, retrying', self.app_name, exc_info=True 149 ) 150 time.sleep(1) 151 if not started: 152 raise jsonrpc_client_base.AppRestoreConnectionError( 153 self._ad, 154 '%s failed to connect for %s at host port %s, device port %s' 155 % (self.app_name, self._adb.serial, self.host_port, self.device_port), 156 ) 157 158 def _start_event_client(self): 159 # Start an EventDispatcher for the current sl4a session 160 event_client = Sl4aClient(self._ad) 161 event_client.host_port = self.host_port 162 event_client.device_port = self.device_port 163 event_client.connect( 164 uid=self.uid, cmd=jsonrpc_client_base.JsonRpcCommand.CONTINUE 165 ) 166 ed = event_dispatcher.EventDispatcher(event_client) 167 ed.start() 168 return ed 169