text: # Copyright 2016 Google Inc.
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  #     http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  """JSON RPC interface to android scripting engine."""
  
  import time
  
  from mobly import utils
  from mobly.controllers.android_device_lib import event_dispatcher
  from mobly.controllers.android_device_lib import jsonrpc_client_base
  
  _APP_NAME = 'SL4A'
  _DEVICE_SIDE_PORT = 8080
  _LAUNCH_CMD = (
      'am start -a com.googlecode.android_scripting.action.LAUNCH_SERVER '
      '--ei com.googlecode.android_scripting.extra.USE_SERVICE_PORT %s '
      'com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher')
  # Maximum time to wait for the app to start on the device (10 minutes).
  # TODO: This timeout is set high in order to allow for retries in
  # start_app_and_connect. Decrease it when the call to connect() has the option
  # for a quicker timeout than the default _cmd() timeout.
  # TODO: Evaluate whether the high timeout still makes sense for sl4a. It was
  # designed for user snippets which could be very slow to start depending on the
  # size of the snippet and main apps. sl4a can probably use a much smaller value.
  _APP_START_WAIT_TIME = 2 * 60
  
  
  class Sl4aClient(jsonrpc_client_base.JsonRpcClientBase):
    """A client for interacting with SL4A using Mobly Snippet Lib.
  
    Extra public attributes:
    ed: Event dispatcher instance for this sl4a client.
    """
  
    def __init__(self, ad):
      """Initializes an Sl4aClient.
  
      Args:
        ad: AndroidDevice object.
      """
      super(Sl4aClient, self).__init__(app_name=_APP_NAME, ad=ad)
      self._ad = ad
      self.ed = None
      self._adb = ad.adb
  
    def start_app_and_connect(self):
      """Overrides superclass."""
      # Check that sl4a is installed
      out = self._adb.shell('pm list package')
      if not utils.grep('com.googlecode.android_scripting', out):
        raise jsonrpc_client_base.AppStartError(
            self._ad, '%s is not installed on %s' % (_APP_NAME, self._adb.serial))
      self.disable_hidden_api_blacklist()
  
      # sl4a has problems connecting after disconnection, so kill the apk and
      # try connecting again.
      try:
        self.stop_app()
      except Exception as e:
        self.log.warning(e)
  
      # Launch the app
      self.device_port = _DEVICE_SIDE_PORT
      self._adb.shell(_LAUNCH_CMD % self.device_port)
  
      # Try to start the connection (not restore the connectivity).
      # The function name restore_app_connection is used here is for the
      # purpose of reusing the same code as it does when restoring the
      # connection. And we do not want to come up with another function
      # name to complicate the API. Change the name if necessary.
      self.restore_app_connection()
  
    def restore_app_connection(self, port=None):
      """Restores the sl4a after device got disconnected.
  
      Instead of creating new instance of the client:
        - Uses the given port (or find a new available host_port if none is
        given).
        - Tries to connect to remote server with selected port.
  
      Args:
        port: If given, this is the host port from which to connect to remote
          device port. If not provided, find a new available port as host
          port.
  
      Raises:
        AppRestoreConnectionError: When the app was not able to be started.
      """
      self.host_port = port or utils.get_available_host_port()
      self._retry_connect()
      self.ed = self._start_event_client()
  
    def stop_app(self):
      """Overrides superclass."""
      try:
        if self._conn:
          # Be polite; let the dest know we're shutting down.
          try:
            self.closeSl4aSession()
          except:
            self.log.exception('Failed to gracefully shut down %s.',
                               self.app_name)
  
          # Close the socket connection.
          self.disconnect()
          self.stop_event_dispatcher()
  
        # Terminate the app
        self._adb.shell('am force-stop com.googlecode.android_scripting')
      finally:
        # Always clean up the adb port
        self.clear_host_port()
  
    def stop_event_dispatcher(self):
      # Close Event Dispatcher
      if self.ed:
        try:
          self.ed.clean_up()
        except:
          self.log.exception('Failed to shutdown sl4a event dispatcher.')
        self.ed = None
  
    def _retry_connect(self):
      self._adb.forward(['tcp:%d' % self.host_port, 'tcp:%d' % self.device_port])
      start_time = time.time()
      expiration_time = start_time + _APP_START_WAIT_TIME
      started = False
      while time.time() < expiration_time:
        self.log.debug('Attempting to start %s.', self.app_name)
        try:
          self.connect()
          started = True
          break
        except:
          self.log.debug('%s is not yet running, retrying',
                         self.app_name,
                         exc_info=True)
        time.sleep(1)
      if not started:
        raise jsonrpc_client_base.AppRestoreConnectionError(
            self._ad, '%s failed to connect for %s at host port %s, '
            'device port %s' %
            (self.app_name, self._adb.serial, self.host_port, self.device_port))
  
    def _start_event_client(self):
      # Start an EventDispatcher for the current sl4a session
      event_client = Sl4aClient(self._ad)
      event_client.host_port = self.host_port
      event_client.device_port = self.device_port
      event_client.connect(uid=self.uid,
                           cmd=jsonrpc_client_base.JsonRpcCommand.CONTINUE)
      ed = event_dispatcher.EventDispatcher(event_client)
      ed.start()
      return ed
  