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