Package com :: Package dtmilano :: Package android :: Module viewclient |
[hide private]
[frames] | no frames]

# Source Code for Module com.dtmilano.android.viewclient

       1  # -*- coding: utf-8 -*-
       2  '''
       3  Copyright (C) 2012-2014  Diego Torres Milano
       4  Created on Feb 2, 2012
       5
       6  Licensed under the Apache License, Version 2.0 (the "License");
       7  you may not use this file except in compliance with the License.
       8  You may obtain a copy of the License at
       9
      10         http://www.apache.org/licenses/LICENSE-2.0
      11
      12  Unless required by applicable law or agreed to in writing, software
      13  distributed under the License is distributed on an "AS IS" BASIS,
      14  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      15  See the License for the specific language governing permissions and
      16  limitations under the License.
      17
      18  @author: Diego Torres Milano
      19  '''
      20
      21  __version__ = '5.6.3'
      22
      23  import sys
      24  import warnings
      25  if sys.executable:
      26      if 'monkeyrunner' in sys.executable:
      27          warnings.warn(
      28      '''
      29
      30      You should use a 'python' interpreter, not 'monkeyrunner' for this module
      31
      32      ''', RuntimeWarning)
      33  import subprocess
      34  import re
      35  import socket
      36  import os
      37  import types
      38  import time
      39  import signal
      40  import copy
      41  import pickle
      42  import platform
      43  import xml.parsers.expat
      44  from com.dtmilano.android.adb import adbclient
      45
      46  DEBUG = False
      47  DEBUG_DEVICE = DEBUG and False
      48  DEBUG_RECEIVED = DEBUG and False
      49  DEBUG_TREE = DEBUG and False
      50  DEBUG_GETATTR = DEBUG and False
      51  DEBUG_CALL = DEBUG and False
      52  DEBUG_COORDS = DEBUG and False
      53  DEBUG_TOUCH = DEBUG and False
      54  DEBUG_STATUSBAR = DEBUG and False
      55  DEBUG_WINDOWS = DEBUG and False
      56  DEBUG_BOUNDS = DEBUG and False
      57  DEBUG_DISTANCE = DEBUG and False
      58
      59  WARNINGS = False
      60
      61  VIEW_SERVER_HOST = 'localhost'
      62  VIEW_SERVER_PORT = 4939
      63
      64  ADB_DEFAULT_PORT = 5555
      65
      66  OFFSET = 25
      67  ''' This assumes the smallest touchable view on the screen is approximately 50px x 50px
      68      and touches it at M{(x+OFFSET, y+OFFSET)} '''
      69
      70  USE_ADB_CLIENT_TO_GET_BUILD_PROPERTIES = True
      71  ''' Use C{AdbClient} to obtain the needed properties. If this is
      72      C{False} then C{adb shell getprop} is used '''
      73
      74  SKIP_CERTAIN_CLASSES_IN_GET_XY_ENABLED = False
      75  ''' Skips some classes related with the Action Bar and the PhoneWindow$DecorView in the
      76      coordinates calculation
      77      @see: L{View.getXY()} '''
      78
      79  VIEW_CLIENT_TOUCH_WORKAROUND_ENABLED = False
      80  ''' Under some conditions the touch event should be longer [t(DOWN) << t(UP)]. C{True} enables a
      81      workaround to delay the events.'''
      82
      83  # some device properties
      84  VERSION_SDK_PROPERTY = 'ro.build.version.sdk'
      85  VERSION_RELEASE_PROPERTY = 'ro.build.version.release'
      86
      87  # some constants for the attributes
      88  ID_PROPERTY = 'mID'
      89  ID_PROPERTY_UI_AUTOMATOR = 'uniqueId'
      90  TEXT_PROPERTY = 'text:mText'
      91  TEXT_PROPERTY_API_10 = 'mText'
      92  TEXT_PROPERTY_UI_AUTOMATOR = 'text'
      93  WS = "\xfe" # the whitespace replacement char for TEXT_PROPERTY
      94  LEFT_PROPERTY = 'layout:mLeft'
      95  LEFT_PROPERTY_API_8 = 'mLeft'
      96  TOP_PROPERTY = 'layout:mTop'
      97  TOP_PROPERTY_API_8 = 'mTop'
      98  WIDTH_PROPERTY = 'layout:getWidth()'
      99  WIDTH_PROPERTY_API_8 = 'getWidth()'
     100  HEIGHT_PROPERTY = 'layout:getHeight()'
     101  HEIGHT_PROPERTY_API_8 = 'getHeight()'
     102  GET_VISIBILITY_PROPERTY = 'getVisibility()'
     103  LAYOUT_TOP_MARGIN_PROPERTY = 'layout:layout_topMargin'
     104
     105  # visibility
     106  VISIBLE = 0x0
     107  INVISIBLE = 0x4
     108  GONE = 0x8
     109
     110  RegexType = type(re.compile(''))
     111  IP_RE = re.compile('^(\d{1,3}\.){3}\d{1,3}$')
     112  ID_RE = re.compile('id/([^/]*)(/(\d+))?')
     113
     114 -def _nd(name):
     115      '''
     116      @return: Returns a named decimal regex
     117      '''
     118      return '(?P<%s>\d+)' % name
     119
     120 -def _nh(name):
     121      '''
     122      @return: Returns a named hex regex
     123      '''
     124      return '(?P<%s>[0-9a-f]+)' % name
     125
     126 -def _ns(name, greedy=False):
     127      '''
     128      NOTICE: this is using a non-greedy (or minimal) regex
     129
     130      @type name: str
     131      @param name: the name used to tag the expression
     132      @type greedy: bool
     133      @param greedy: Whether the regex is greedy or not
     134
     135      @return: Returns a named string regex (only non-whitespace characters allowed)
     136      '''
     137      return '(?P<%s>\S+%s)' % (name, '' if greedy else '?')
     138
     139
     140 -class Window:
     141      '''
     142      Window class
     143      '''
     144
     145 -    def __init__(self, num, winId, activity, wvx, wvy, wvw, wvh, px, py, visibility):
     146          '''
     147          Constructor
     148
     149          @type num: int
     150          @param num: Ordering number in Window Manager
     151          @type winId: str
     152          @param winId: the window ID
     153          @type activity: str
     154          @param activity: the activity (or sometimes other component) owning the window
     155          @type wvx: int
     156          @param wvx: window's virtual X
     157          @type wvy: int
     158          @param wvy: window's virtual Y
     159          @type wvw: int
     160          @param wvw: window's virtual width
     161          @type wvh: int
     162          @param wvh: window's virtual height
     163          @type px: int
     164          @param px: parent's X
     165          @type py: int
     166          @param py: parent's Y
     167          @type visibility: int
     168          @param visibility: visibility of the window
     169          '''
     170
     171          if DEBUG_COORDS: print >> sys.stderr, "Window(%d, %s, %s, %d, %d, %d, %d, %d, %d, %d)" % \
     172                  (num, winId, activity, wvx, wvy, wvw, wvh, px, py, visibility)
     173          self.num = num
     174          self.winId = winId
     175          self.activity = activity
     176          self.wvx = wvx
     177          self.wvy = wvy
     178          self.wvw = wvw
     179          self.wvh = wvh
     180          self.px = px
     181          self.py = py
     182          self.visibility = visibility
     183
     184 -    def __str__(self):
     185          return "Window(%d, wid=%s, a=%s, x=%d, y=%d, w=%d, h=%d, px=%d, py=%d, v=%d)" % \
     186                  (self.num, self.winId, self.activity, self.wvx, self.wvy, self.wvw, self.wvh, self.px, self.py, self.visibility)
     187
     188
     189 -class ViewNotFoundException(Exception):
     190      '''
     191      ViewNotFoundException is raised when a View is not found.
     192      '''
     193
     194 -    def __init__(self, attr, value, root):
     195          if isinstance(value, RegexType):
     196              msg = "Couldn't find View with %s that matches '%s' in tree with root=%s" % (attr, value.pattern, root)
     197          else:
     198              msg = "Couldn't find View with %s='%s' in tree with root=%s" % (attr, value, root)
     199          super(Exception, self).__init__(msg)
     200
     201 -class View:
     202      '''
     203      View class
     204      '''
     205
     206      @staticmethod
     207 -    def factory(arg1, arg2, version=-1, forceviewserveruse=False):
     208          '''
     209          View factory
     210
     211          @type arg1: ClassType or dict
     212          @type arg2: View instance or AdbClient
     213          '''
     214
     215          if type(arg1) == types.ClassType:
     216              cls = arg1
     217              attrs = None
     218          else:
     219              cls = None
     220              attrs = arg1
     221          if isinstance(arg2, View):
     222              view = arg2
     223              device = None
     224          else:
     225              device = arg2
     226              view = None
     227
     228          if attrs and attrs.has_key('class'):
     229              clazz = attrs['class']
     230              if clazz == 'android.widget.TextView':
     231                  return TextView(attrs, device, version, forceviewserveruse)
     232              elif clazz == 'android.widget.EditText':
     233                  return EditText(attrs, device, version, forceviewserveruse)
     234              else:
     235                  return View(attrs, device, version, forceviewserveruse)
     236          elif cls:
     237              if view:
     238                  return cls.__copy(view)
     239              else:
     240                  return cls(attrs, device, version, forceviewserveruse)
     241          elif view:
     242              return copy.copy(view)
     243          else:
     244              return View(attrs, device, version, forceviewserveruse)
     245
     246      @classmethod
     247 -    def __copy(cls, view):
     248          '''
     249          Copy constructor
     250          '''
     251
     252          return cls(view.map, view.device, view.version, view.forceviewserveruse)
     253
     254 -    def __init__(self, map, device, version=-1, forceviewserveruse=False):
     255          '''
     256          Constructor
     257
     258          @type map: map
     259          @param map: the map containing the (attribute, value) pairs
     260          @type device: MonkeyDevice
     261          @param device: the device containing this View
     262          @type version: int
     263          @param version: the Android SDK version number of the platform where this View belongs. If
     264                          this is C{-1} then the Android SDK version will be obtained in this
     265                          constructor.
     266          @type forceviewserveruse: boolean
     267          @param forceviewserveruse: Force the use of C{ViewServer} even if the conditions were given
     268                          to use C{UiAutomator}.
     269          '''
     270
     271          self.map = map
     272          ''' The map that contains the C{attr},C{value} pairs '''
     273          self.device = device
     274          ''' The MonkeyDevice '''
     275          self.children = []
     276          ''' The children of this View '''
     277          self.parent = None
     278          ''' The parent of this View '''
     279          self.windows = {}
     280          self.currentFocus = None
     281          ''' The current focus '''
     282          self.build = {}
     283          ''' Build properties '''
     284          self.version = version
     285          ''' API version number '''
     286          self.forceviewserveruse = forceviewserveruse
     287          ''' Force ViewServer use '''
     288
     289          if version != -1:
     290              self.build[VERSION_SDK_PROPERTY] = version
     291          else:
     292              try:
     293                  if USE_ADB_CLIENT_TO_GET_BUILD_PROPERTIES:
     294                      self.build[VERSION_SDK_PROPERTY] = int(device.getProperty(VERSION_SDK_PROPERTY))
     295                  else:
     296                      self.build[VERSION_SDK_PROPERTY] = int(device.shell('getprop ' + VERSION_SDK_PROPERTY)[:-2])
     297              except:
     298                  self.build[VERSION_SDK_PROPERTY] = -1
     299
     300          version = self.build[VERSION_SDK_PROPERTY]
     301          self.useUiAutomator = (version >= 16) and not forceviewserveruse
     302          ''' Whether to use UIAutomator or ViewServer '''
     303          self.idProperty = None
     304          ''' The id property depending on the View attribute format '''
     305          self.textProperty = None
     306          ''' The text property depending on the View attribute format '''
     307          self.leftProperty = None
     308          ''' The left property depending on the View attribute format '''
     309          self.topProperty = None
     310          ''' The top property depending on the View attribute format '''
     311          self.widthProperty = None
     312          ''' The width property depending on the View attribute format '''
     313          self.heightProperty = None
     314          ''' The height property depending on the View attribute format '''
     315          if version >= 16 and self.useUiAutomator:
     316              self.idProperty = ID_PROPERTY_UI_AUTOMATOR
     317              self.textProperty = TEXT_PROPERTY_UI_AUTOMATOR
     318              self.leftProperty = LEFT_PROPERTY
     319              self.topProperty = TOP_PROPERTY
     320              self.widthProperty = WIDTH_PROPERTY
     321              self.heightProperty = HEIGHT_PROPERTY
     322          elif version > 10 and (version < 16 or self.useUiAutomator):
     323              self.idProperty = ID_PROPERTY
     324              self.textProperty = TEXT_PROPERTY
     325              self.leftProperty = LEFT_PROPERTY
     326              self.topProperty = TOP_PROPERTY
     327              self.widthProperty = WIDTH_PROPERTY
     328              self.heightProperty = HEIGHT_PROPERTY
     329          elif version == 10:
     330              self.idProperty = ID_PROPERTY
     331              self.textProperty = TEXT_PROPERTY_API_10
     332              self.leftProperty = LEFT_PROPERTY
     333              self.topProperty = TOP_PROPERTY
     334              self.widthProperty = WIDTH_PROPERTY
     335              self.heightProperty = HEIGHT_PROPERTY
     336          elif version >= 7 and version < 10:
     337              self.idProperty = ID_PROPERTY
     338              self.textProperty = TEXT_PROPERTY_API_10
     339              self.leftProperty = LEFT_PROPERTY_API_8
     340              self.topProperty = TOP_PROPERTY_API_8
     341              self.widthProperty = WIDTH_PROPERTY_API_8
     342              self.heightProperty = HEIGHT_PROPERTY_API_8
     343          elif version > 0 and version < 7:
     344              self.idProperty = ID_PROPERTY
     345              self.textProperty = TEXT_PROPERTY_API_10
     346              self.leftProperty = LEFT_PROPERTY
     347              self.topProperty = TOP_PROPERTY
     348              self.widthProperty = WIDTH_PROPERTY
     349              self.heightProperty = HEIGHT_PROPERTY
     350          elif version == -1:
     351              self.idProperty = ID_PROPERTY
     352              self.textProperty = TEXT_PROPERTY
     353              self.leftProperty = LEFT_PROPERTY
     354              self.topProperty = TOP_PROPERTY
     355              self.widthProperty = WIDTH_PROPERTY
     356              self.heightProperty = HEIGHT_PROPERTY
     357          else:
     358              self.idProperty = ID_PROPERTY
     359              self.textProperty = TEXT_PROPERTY
     360              self.leftProperty = LEFT_PROPERTY
     361              self.topProperty = TOP_PROPERTY
     362              self.widthProperty = WIDTH_PROPERTY
     363              self.heightProperty = HEIGHT_PROPERTY
     364
     365 -    def __getitem__(self, key):
     366          return self.map[key]
     367
     368 -    def __getattr__(self, name):
     369          if DEBUG_GETATTR:
     370              print >>sys.stderr, "__getattr__(%s)    version: %d" % (name, self.build[VERSION_SDK_PROPERTY])
     371
     372          # NOTE:
     373          # I should try to see if 'name' is a defined method
     374          # but it seems that if I call locals() here an infinite loop is entered
     375
     376          if self.map.has_key(name):
     377              r = self.map[name]
     378          elif self.map.has_key(name + '()'):
     379              # the method names are stored in the map with their trailing '()'
     380              r = self.map[name + '()']
     381          elif name.count("_") > 0:
     382              mangledList = self.allPossibleNamesWithColon(name)
     383              mangledName = self.intersection(mangledList, self.map.keys())
     384              if len(mangledName) > 0 and self.map.has_key(mangledName[0]):
     385                  r = self.map[mangledName[0]]
     386              else:
     387                  # Default behavior
     388                  raise AttributeError, name
     389          else:
     390              # try removing 'is' prefix
     391              if DEBUG_GETATTR:
     392                  print >> sys.stderr, "    __getattr__: trying without 'is' prefix"
     393              suffix = name[2:].lower()
     394              if self.map.has_key(suffix):
     395                  r = self.map[suffix]
     396              else:
     397                  # Default behavior
     398                  raise AttributeError, name
     399
     400          # if the method name starts with 'is' let's assume its return value is boolean
     401  #         if name[:2] == 'is':
     402  #             r = True if r == 'true' else False
     403          if r == 'true':
     404              r = True
     405          elif r == 'false':
     406              r = False
     407
     408          # this should not cached in some way
     409          def innerMethod():
     410              if DEBUG_GETATTR:
     411                  print >>sys.stderr, "innerMethod: %s returning %s" % (innerMethod.__name__, r)
     412              return r
     413
     414          innerMethod.__name__ = name
     415
     416          # this should work, but then there's problems with the arguments of innerMethod
     417          # even if innerMethod(self) is added
     418          #setattr(View, innerMethod.__name__, innerMethod)
     419          #setattr(self, innerMethod.__name__, innerMethod)
     420
     421          return innerMethod
     422
     423 -    def __call__(self, *args, **kwargs):
     424          if DEBUG_CALL:
     425              print >>sys.stderr, "__call__(%s)" % (args if args else None)
     426
     427 -    def getClass(self):
     428          '''
     429          Gets the L{View} class
     430
     431          @return:  the L{View} class or C{None} if not defined
     432          '''
     433
     434          try:
     435              return self.map['class']
     436          except:
     437              return None
     438
     439 -    def getId(self):
     440          '''
     441          Gets the L{View} Id
     442
     443          @return: the L{View} C{Id} or C{None} if not defined
     444          @see: L{getUniqueId()}
     445          '''
     446
     447          try:
     448              return self.map['resource-id']
     449          except:
     450              pass
     451
     452          try:
     453              return self.map[self.idProperty]
     454          except:
     455              return None
     456
     457 -    def getContentDescription(self):
     458          '''
     459          Gets the content description.
     460          '''
     461
     462          try:
     463              return self.map['content-desc']
     464          except:
     465              return None
     466
     467 -    def getParent(self):
     468          '''
     469          Gets the parent.
     470          '''
     471
     472          return self.parent
     473
     474 -    def getText(self):
     475          '''
     476          Gets the text attribute.
     477
     478          @return: the text attribute or C{None} if not defined
     479          '''
     480
     481          try:
     482              return self.map[self.textProperty]
     483          except Exception:
     484              return None
     485
     486 -    def getHeight(self):
     487          '''
     488          Gets the height.
     489          '''
     490
     491          if self.useUiAutomator:
     492              return self.map['bounds'][1][1] - self.map['bounds'][0][1]
     493          else:
     494              try:
     495                  return int(self.map[self.heightProperty])
     496              except:
     497                  return 0
     498
     499 -    def getWidth(self):
     500          '''
     501          Gets the width.
     502          '''
     503
     504          if self.useUiAutomator:
     505              return self.map['bounds'][1][0] - self.map['bounds'][0][0]
     506          else:
     507              try:
     508                  return int(self.map[self.widthProperty])
     509              except:
     510                  return 0
     511
     512 -    def getUniqueId(self):
     513          '''
     514          Gets the unique Id of this View.
     515
     516          @see: L{ViewClient.__splitAttrs()} for a discussion on B{Unique Ids}
     517          '''
     518
     519          try:
     520              return self.map['uniqueId']
     521          except:
     522              return None
     523
     524 -    def getVisibility(self):
     525          '''
     526          Gets the View visibility
     527          '''
     528
     529          try:
     530              if self.map[GET_VISIBILITY_PROPERTY] == 'VISIBLE':
     531                  return VISIBLE
     532              elif self.map[GET_VISIBILITY_PROPERTY] == 'INVISIBLE':
     533                  return INVISIBLE
     534              elif self.map[GET_VISIBILITY_PROPERTY] == 'GONE':
     535                  return GONE
     536              else:
     537                  return -2
     538          except:
     539              return -1
     540
     541 -    def getX(self):
     542          '''
     543          Gets the View X coordinate
     544          '''
     545
     546          if DEBUG_COORDS:
     547              print >>sys.stderr, "getX(%s %s ## %s)" % (self.getClass(), self.getId(), self.getUniqueId())
     548          x = 0
     549
     550          if self.useUiAutomator:
     551              x = self.map['bounds'][0][0]
     552          else:
     553              try:
     554                  if GET_VISIBILITY_PROPERTY in self.map and self.map[GET_VISIBILITY_PROPERTY] == 'VISIBLE':
     555                      _x = int(self.map[self.leftProperty])
     556                      if DEBUG_COORDS: print >>sys.stderr, "   getX: VISIBLE adding %d" % _x
     557                      x += _x
     558              except:
     559                  warnings.warn("View %s has no '%s' property" % (self.getId(), self.leftProperty))
     560
     561          if DEBUG_COORDS: print >>sys.stderr, "   getX: returning %d" % (x)
     562          return x
     563
     564 -    def getY(self):
     565          '''
     566          Gets the View Y coordinate
     567          '''
     568
     569          if DEBUG_COORDS:
     570              print >>sys.stderr, "getY(%s %s ## %s)" % (self.getClass(), self.getId(), self.getUniqueId())
     571          y = 0
     572
     573          if self.useUiAutomator:
     574              y = self.map['bounds'][0][1]
     575          else:
     576              try:
     577                  if GET_VISIBILITY_PROPERTY in self.map and self.map[GET_VISIBILITY_PROPERTY] == 'VISIBLE':
     578                      _y = int(self.map[self.topProperty])
     579                      if DEBUG_COORDS: print >>sys.stderr, "   getY: VISIBLE adding %d" % _y
     580                      y += _y
     581              except:
     582                  warnings.warn("View %s has no '%s' property" % (self.getId(), self.topProperty))
     583
     584          if DEBUG_COORDS: print >>sys.stderr, "   getY: returning %d" % (y)
     585          return y
     586
     587 -    def getXY(self, debug=False):
     588          '''
     589          Returns the I{screen} coordinates of this C{View}.
     590
     591          @return: The I{screen} coordinates of this C{View}
     592          '''
     593
     594          if DEBUG_COORDS or debug:
     595              try:
     596                  id = self.getId()
     597              except:
     598                  id = "NO_ID"
     599              print >> sys.stderr, "getXY(%s %s ## %s)" % (self.getClass(), id, self.getUniqueId())
     600
     601          x = self.getX()
     602          y = self.getY()
     603          if self.useUiAutomator:
     604              return (x, y)
     605
     606          parent = self.parent
     607          if DEBUG_COORDS: print >> sys.stderr, "   getXY: x=%s y=%s parent=%s" % (x, y, parent.getUniqueId() if parent else "None")
     608          hx = 0
     609          ''' Hierarchy accumulated X '''
     610          hy = 0
     611          ''' Hierarchy accumulated Y '''
     612
     613          if DEBUG_COORDS: print >> sys.stderr, "   getXY: not using UiAutomator, calculating parent coordinates"
     614          while parent != None:
     615              if DEBUG_COORDS: print >> sys.stderr, "      getXY: parent: %s %s <<<<" % (parent.getClass(), parent.getId())
     616              if SKIP_CERTAIN_CLASSES_IN_GET_XY_ENABLED:
     617                  if parent.getClass() in [ 'com.android.internal.widget.ActionBarView',
     618                                     'com.android.internal.widget.ActionBarContextView',
     619                                     'com.android.internal.view.menu.ActionMenuView',
     620                                     'com.android.internal.policy.impl.PhoneWindow$DecorView' ]:
     621                      if DEBUG_COORDS: print >> sys.stderr, "   getXY: skipping %s %s (%d,%d)" % (parent.getClass(), parent.getId(), parent.getX(), parent.getY())
     622                      parent = parent.parent
     623                      continue
     624              if DEBUG_COORDS: print >> sys.stderr, "   getXY: parent=%s x=%d hx=%d y=%d hy=%d" % (parent.getId(), x, hx, y, hy)
     625              hx += parent.getX()
     626              hy += parent.getY()
     627              parent = parent.parent
     628
     629          (wvx, wvy) = self.__dumpWindowsInformation(debug=debug)
     630          if DEBUG_COORDS or debug:
     631              print >>sys.stderr, "   getXY: wv=(%d, %d) (windows information)" % (wvx, wvy)
     632          try:
     633              fw = self.windows[self.currentFocus]
     634              if DEBUG_STATUSBAR:
     635                  print >> sys.stderr, "    getXY: focused window=", fw
     636                  print >> sys.stderr, "    getXY: deciding whether to consider statusbar offset because current focused windows is at", (fw.wvx, fw.wvy), "parent", (fw.px, fw.py)
     637          except KeyError:
     638              fw = None
     639          (sbw, sbh) = self.__obtainStatusBarDimensionsIfVisible()
     640          if DEBUG_COORDS or debug:
     641              print >>sys.stderr, "   getXY: sb=(%d, %d) (statusbar dimensions)" % (sbw, sbh)
     642          statusBarOffset = 0
     643          pwx = 0
     644          pwy = 0
     645
     646          if fw:
     647              if DEBUG_COORDS:
     648                  print >>sys.stderr, "    getXY: focused window=", fw, "sb=", (sbw, sbh)
     649              if fw.wvy <= sbh: # it's very unlikely that fw.wvy < sbh, that is a window over the statusbar
     650                  if DEBUG_STATUSBAR: print >>sys.stderr, "        getXY: yes, considering offset=", sbh
     651                  statusBarOffset = sbh
     652              else:
     653                  if DEBUG_STATUSBAR: print >>sys.stderr, "        getXY: no, ignoring statusbar offset fw.wvy=", fw.wvy, ">", sbh
     654
     655              if fw.py == fw.wvy:
     656                  if DEBUG_STATUSBAR: print >>sys.stderr, "        getXY: but wait, fw.py == fw.wvy so we are adjusting by ", (fw.px, fw.py)
     657                  pwx = fw.px
     658                  pwy = fw.py
     659              else:
     660                  if DEBUG_STATUSBAR: print >>sys.stderr, "    getXY: fw.py=%d <= fw.wvy=%d, no adjustment" % (fw.py, fw.wvy)
     661
     662          if DEBUG_COORDS or DEBUG_STATUSBAR or debug:
     663              print >>sys.stderr, "   getXY: returning (%d, %d) ***" % (x+hx+wvx+pwx, y+hy+wvy-statusBarOffset+pwy)
     664              print >>sys.stderr, "                     x=%d+%d+%d+%d" % (x,hx,wvx,pwx)
     665              print >>sys.stderr, "                     y=%d+%d+%d-%d+%d" % (y,hy,wvy,statusBarOffset,pwy)
     666          return (x+hx+wvx+pwx, y+hy+wvy-statusBarOffset+pwy)
     667
     668 -    def getCoords(self):
     669          '''
     670          Gets the coords of the View
     671
     672          @return: A tuple containing the View's coordinates ((L, T), (R, B))
     673          '''
     674
     675          if DEBUG_COORDS:
     676              print >>sys.stderr, "getCoords(%s %s ## %s)" % (self.getClass(), self.getId(), self.getUniqueId())
     677
     678          (x, y) = self.getXY();
     679          w = self.getWidth()
     680          h = self.getHeight()
     681          return ((x, y), (x+w, y+h))
     682
     683 -    def getPositionAndSize(self):
     684          '''
     685          Gets the position and size (X,Y, W, H)
     686
     687          @return: A tuple containing the View's coordinates (X, Y, W, H)
     688          '''
     689
     690          (x, y) = self.getXY();
     691          w = self.getWidth()
     692          h = self.getHeight()
     693          return (x, y, w, h)
     694
     695
     696 -    def getCenter(self):
     697          '''
     698          Gets the center coords of the View
     699
     700          @author: U{Dean Morin <https://github.com/deanmorin>}
     701          '''
     702
     703          (left, top), (right, bottom) = self.getCoords()
     704          x = left + (right - left) / 2
     705          y = top + (bottom - top) / 2
     706          return (x, y)
     707
     708 -    def __obtainStatusBarDimensionsIfVisible(self):
     709          sbw = 0
     710          sbh = 0
     711          for winId in self.windows:
     712              w = self.windows[winId]
     713              if DEBUG_COORDS: print >> sys.stderr, "      __obtainStatusBarDimensionsIfVisible: w=", w, "   w.activity=", w.activity, "%%%"
     714              if w.activity == 'StatusBar':
     715                  if w.wvy == 0 and w.visibility == 0:
     716                      if DEBUG_COORDS: print >> sys.stderr, "      __obtainStatusBarDimensionsIfVisible: statusBar=", (w.wvw, w.wvh)
     717                      sbw = w.wvw
     718                      sbh = w.wvh
     719                  break
     720
     721          return (sbw, sbh)
     722
     723 -    def __obtainVxVy(self, m):
     724          wvx = int(m.group('vx'))
     725          wvy = int(m.group('vy'))
     726          return wvx, wvy
     727
     728 -    def __obtainVwVh(self, m):
     729          (wvx, wvy) = self.__obtainVxVy(m)
     730          wvx1 = int(m.group('vx1'))
     731          wvy1 = int(m.group('vy1'))
     732          return (wvx1-wvx, wvy1-wvy)
     733
     734 -    def __obtainPxPy(self, m):
     735          px = int(m.group('px'))
     736          py = int(m.group('py'))
     737          return (px, py)
     738
     739 -    def __dumpWindowsInformation(self, debug=False):
     740          self.windows = {}
     741          self.currentFocus = None
     742          dww = self.device.shell('dumpsys window windows')
     743          if DEBUG_WINDOWS or debug: print >> sys.stderr, dww
     744          lines = dww.split('\n')
     745          widRE = re.compile('^ *Window #%s Window{%s (u\d+ )?%s?.*}:' %
     746                              (_nd('num'), _nh('winId'), _ns('activity', greedy=True)))
     747          currentFocusRE = re.compile('^  mCurrentFocus=Window{%s .*' % _nh('winId'))
     748          viewVisibilityRE = re.compile(' mViewVisibility=0x%s ' % _nh('visibility'))
     749          # This is for 4.0.4 API-15
     750          containingFrameRE = re.compile('^   *mContainingFrame=\[%s,%s\]\[%s,%s\] mParentFrame=\[%s,%s\]\[%s,%s\]' %
     751                               (_nd('cx'), _nd('cy'), _nd('cw'), _nd('ch'), _nd('px'), _nd('py'), _nd('pw'), _nd('ph')))
     752          contentFrameRE = re.compile('^   *mContentFrame=\[%s,%s\]\[%s,%s\] mVisibleFrame=\[%s,%s\]\[%s,%s\]' %
     753                               (_nd('x'), _nd('y'), _nd('w'), _nd('h'), _nd('vx'), _nd('vy'), _nd('vx1'), _nd('vy1')))
     754          # This is for 4.1 API-16
     755          framesRE = re.compile('^   *Frames: containing=\[%s,%s\]\[%s,%s\] parent=\[%s,%s\]\[%s,%s\]' %
     756                                 (_nd('cx'), _nd('cy'), _nd('cw'), _nd('ch'), _nd('px'), _nd('py'), _nd('pw'), _nd('ph')))
     757          contentRE = re.compile('^     *content=\[%s,%s\]\[%s,%s\] visible=\[%s,%s\]\[%s,%s\]' %
     758                                 (_nd('x'), _nd('y'), _nd('w'), _nd('h'), _nd('vx'), _nd('vy'), _nd('vx1'), _nd('vy1')))
     759          policyVisibilityRE = re.compile('mPolicyVisibility=%s ' % _ns('policyVisibility', greedy=True))
     760
     761          for l in range(len(lines)):
     762              m = widRE.search(lines[l])
     763              if m:
     764                  num = int(m.group('num'))
     765                  winId = m.group('winId')
     766                  activity = m.group('activity')
     767                  wvx = 0
     768                  wvy = 0
     769                  wvw = 0
     770                  wvh = 0
     771                  px = 0
     772                  py = 0
     773                  visibility = -1
     774                  policyVisibility = 0x0
     775
     776                  for l2 in range(l+1, len(lines)):
     777                      m = widRE.search(lines[l2])
     778                      if m:
     779                          l += (l2-1)
     780                          break
     781                      m = viewVisibilityRE.search(lines[l2])
     782                      if m:
     783                          visibility = int(m.group('visibility'))
     784                          if DEBUG_COORDS: print >> sys.stderr, "__dumpWindowsInformation: visibility=", visibility
     785                      if self.build[VERSION_SDK_PROPERTY] >= 17:
     786                          wvx, wvy = (0, 0)
     787                          wvw, wvh = (0, 0)
     788                      if self.build[VERSION_SDK_PROPERTY] >= 16:
     789                          m = framesRE.search(lines[l2])
     790                          if m:
     791                              px, py = self.__obtainPxPy(m)
     792                              m = contentRE.search(lines[l2+1])
     793                              if m:
     794                                  # FIXME: the information provided by 'dumpsys window windows' in 4.2.1 (API 16)
     795                                  # when there's a system dialog may not be correct and causes the View coordinates
     796                                  # be offset by this amount, see
     797                                  # https://github.com/dtmilano/AndroidViewClient/issues/29
     798                                  wvx, wvy = self.__obtainVxVy(m)
     799                                  wvw, wvh = self.__obtainVwVh(m)
     800                      elif self.build[VERSION_SDK_PROPERTY] == 15:
     801                          m = containingFrameRE.search(lines[l2])
     802                          if m:
     803                              px, py = self.__obtainPxPy(m)
     804                              m = contentFrameRE.search(lines[l2+1])
     805                              if m:
     806                                  wvx, wvy = self.__obtainVxVy(m)
     807                                  wvw, wvh = self.__obtainVwVh(m)
     808                      elif self.build[VERSION_SDK_PROPERTY] == 10:
     809                          m = containingFrameRE.search(lines[l2])
     810                          if m:
     811                              px, py = self.__obtainPxPy(m)
     812                              m = contentFrameRE.search(lines[l2+1])
     813                              if m:
     814                                  wvx, wvy = self.__obtainVxVy(m)
     815                                  wvw, wvh = self.__obtainVwVh(m)
     816                      else:
     817                          warnings.warn("Unsupported Android version %d" % self.build[VERSION_SDK_PROPERTY])
     818
     819                      #print >> sys.stderr, "Searching policyVisibility in", lines[l2]
     820                      m = policyVisibilityRE.search(lines[l2])
     821                      if m:
     822                          policyVisibility = 0x0 if m.group('policyVisibility') == 'true' else 0x8
     823
     824                  self.windows[winId] = Window(num, winId, activity, wvx, wvy, wvw, wvh, px, py, visibility + policyVisibility)
     825              else:
     826                  m = currentFocusRE.search(lines[l])
     827                  if m:
     828                      self.currentFocus = m.group('winId')
     829
     830          if self.currentFocus in self.windows and self.windows[self.currentFocus].visibility == 0:
     831              if DEBUG_COORDS or debug:
     832                  print >> sys.stderr, "__dumpWindowsInformation: focus=", self.currentFocus
     833                  print >> sys.stderr, "__dumpWindowsInformation:", self.windows[self.currentFocus]
     834              w = self.windows[self.currentFocus]
     835              return (w.wvx, w.wvy)
     836          else:
     837              if DEBUG_COORDS: print >> sys.stderr, "__dumpWindowsInformation: (0,0)"
     838              return (0,0)
     839
     840 -    def touch(self, type=adbclient.DOWN_AND_UP):
     841          '''
     842          Touches the center of this C{View}
     843          '''
     844
     845          (x, y) = self.getCenter()
     846          if DEBUG_TOUCH:
     847              print >>sys.stderr, "should touch @ (%d, %d)" % (x, y)
     848          if VIEW_CLIENT_TOUCH_WORKAROUND_ENABLED and type == adbclient.DOWN_AND_UP:
     849              if WARNINGS:
     850                  print >> sys.stderr, "ViewClient: touch workaround enabled"
     851              self.device.touch(x, y, adbclient.DOWN)
     852              time.sleep(50/1000.0)
     853              self.device.touch(x+10, y+10, adbclient.UP)
     854          else:
     855              self.device.touch(x, y, type)
     856
     857 -    def allPossibleNamesWithColon(self, name):
     858          l = []
     859          for i in range(name.count("_")):
     860              name = name.replace("_", ":", 1)
     861              l.append(name)
     862          return l
     863
     864 -    def intersection(self, l1, l2):
     865          return list(set(l1) & set(l2))
     866
     867 -    def containsPoint(self, (x, y)):
     868          (X, Y, W, H) = self.getPositionAndSize()
     869          return (((x >= X) and (x <= (X+W)) and ((y >= Y) and (y <= (Y+H)))))
     870
     871 -    def add(self, child):
     872          '''
     873          Adds a child
     874
     875          @type child: View
     876          @param child: The child to add
     877          '''
     878          child.parent = self
     879          self.children.append(child)
     880
     881 -    def isClickable(self):
     882          return self.__getattr__('isClickable')()
     883
     884 -    def variableNameFromId(self):
     885          _id = self.getId()
     886          if _id:
     887              var = _id.replace('.', '_').replace(':', '___').replace('/', '_')
     888          else:
     889              _id = self.getUniqueId()
     890              m = ID_RE.match(_id)
     891              if m:
     892                  var = m.group(1)
     893                  if m.group(3):
     894                      var += m.group(3)
     895                  if re.match('^\d', var):
     896                      var = 'id_' + var
     897          return var
     898
     899 -    def writeImageToFile(self, filename, format="PNG"):
     900          '''
     901          Write the View image to the specified filename in the specified format.
     902
     903          @type filename: str
     904          @param filename: Absolute path and optional filename receiving the image. If this points to
     905                           a directory, then the filename is determined by this View unique ID and
     906                           format extension.
     907          @type format: str
     908          @param format: Image format (default format is PNG)
     909          '''
     910
     911          if not os.path.isabs(filename):
     912              raise ValueError("writeImageToFile expects an absolute path")
     913          if os.path.isdir(filename):
     914              filename = os.path.join(filename, self.variableNameFromId() + '.' + format.lower())
     915          if DEBUG:
     916              print >> sys.stderr, "writeImageToFile: saving image to '%s' in %s format" % (filename, format)
     917          #self.device.takeSnapshot().getSubImage(self.getPositionAndSize()).writeToFile(filename, format)
     918          # crop:
     919          # im.crop(box) ⇒ image
     920          # Returns a copy of a rectangular region from the current image.
     921          # The box is a 4-tuple defining the left, upper, right, and lower pixel coordinate.
     922          ((l, t), (r, b)) = self.getCoords()
     923          box = (l, t, r, b)
     924          if DEBUG:
     925              print >> sys.stderr, "writeImageToFile: cropping", box, "    reconnect=", self.device.reconnect
     926          self.device.takeSnapshot(reconnect=self.device.reconnect).crop(box).save(filename, format)
     927
     928 -    def __smallStr__(self):
     929          __str = unicode("View[", 'utf-8', 'replace')
     930          if "class" in self.map:
     931              __str += " class=" + self.map['class']
     932          __str += " id=%s" % self.getId()
     933          __str += " ]   parent="
     934          if self.parent and "class" in self.parent.map:
     935              __str += "%s" % self.parent.map["class"]
     936          else:
     937              __str += "None"
     938
     939          return __str
     940
     941 -    def __tinyStr__(self):
     942          __str = unicode("View[", 'utf-8', 'replace')
     943          if "class" in self.map:
     944              __str += " class=" + re.sub('.*\.', '', self.map['class'])
     945          __str += " id=%s" % self.getId()
     946          __str += " ]"
     947
     948          return __str
     949
     950 -    def __microStr__(self):
     951          __str = unicode('', 'utf-8', 'replace')
     952          if "class" in self.map:
     953              __str += re.sub('.*\.', '', self.map['class'])
     954          id = self.getId().replace('id/no_id/', '-')
     955          __str += id
     956          ((L, T), (R, B)) = self.getCoords()
     957          __str += '@%04d%04d%04d%04d' % (L, T, R, B)
     958          __str += ''
     959
     960          return __str
     961
     962
     963 -    def __str__(self):
     964          __str = unicode("View[", 'utf-8', 'replace')
     965          if "class" in self.map:
     966              __str += " class=" + self.map["class"].__str__() + " "
     967          for a in self.map:
     968              __str += a + "="
     969              # decode() works only on python's 8-bit strings
     970              if isinstance(self.map[a], unicode):
     971                  __str += self.map[a]
     972              else:
     973                  __str += unicode(str(self.map[a]), 'utf-8', errors='replace')
     974              __str += " "
     975          __str += "]   parent="
     976          if self.parent:
     977              if "class" in self.parent.map:
     978                  __str += "%s" % self.parent.map["class"]
     979              else:
     980                  __str += self.parent.getId().__str__()
     981          else:
     982              __str += "None"
     983
     984          return __str
     985
     986 -class TextView(View):
     987      '''
     988      TextView class.
     989      '''
     990
     991      pass
     992
     993 -class EditText(TextView):
     994      '''
     995      EditText class.
     996      '''
     997
     998 -    def type(self, text):
     999          self.touch()
    1000          time.sleep(0.5)
    1001          escaped = text.replace('%s', '\\%s')
    1002          encoded = escaped.replace(' ', '%s')
    1003          self.device.type(encoded)
    1004          time.sleep(0.5)
    1005
    1006 -    def backspace(self):
    1007          self.touch()
    1008          time.sleep(1)
    1009          self.device.press('KEYCODE_DEL', adbclient.DOWN_AND_UP)
    1010
    1011 -class UiAutomator2AndroidViewClient():
    1012      '''
    1013      UiAutomator XML to AndroidViewClient
    1014      '''
    1015
    1016 -    def __init__(self, device, version):
    1017          self.device = device
    1018          self.version = version
    1019          self.root = None
    1020          self.nodeStack = []
    1021          self.parent = None
    1022          self.views = []
    1023          self.idCount = 1
    1024
    1025 -    def StartElement(self, name, attributes):
    1026          '''
    1027          Expat start element event handler
    1028          '''
    1029          if name == 'hierarchy':
    1030              pass
    1031          elif name == 'node':
    1032              # Instantiate an Element object
    1033              attributes['uniqueId'] = 'id/no_id/%d' % self.idCount
    1034              bounds = re.split('[\][,]', attributes['bounds'])
    1035              attributes['bounds'] = ((int(bounds[1]), int(bounds[2])), (int(bounds[4]), int(bounds[5])))
    1036              if DEBUG_BOUNDS:
    1037                  print >> sys.stderr, "bounds=", attributes['bounds']
    1038              self.idCount += 1
    1039              child = View.factory(attributes, self.device, self.version)
    1040              self.views.append(child)
    1041              # Push element onto the stack and make it a child of parent
    1042              if not self.nodeStack:
    1043                  self.root = child
    1044              else:
    1045                  self.parent = self.nodeStack[-1]
    1046                  self.parent.add(child)
    1047              self.nodeStack.append(child)
    1048
    1049 -    def EndElement(self, name):
    1050          '''
    1051          Expat end element event handler
    1052          '''
    1053
    1054          if name == 'hierarchy':
    1055              pass
    1056          elif name == 'node':
    1057              self.nodeStack.pop()
    1058
    1059 -    def CharacterData(self, data):
    1060          '''
    1061          Expat character data event handler
    1062          '''
    1063
    1064          if data.strip():
    1065              data = data.encode()
    1066              element = self.nodeStack[-1]
    1067              element.cdata += data
    1068
    1069 -    def Parse(self, uiautomatorxml):
    1070          # Create an Expat parser
    1071          parser = xml.parsers.expat.ParserCreate()
    1072          # Set the Expat event handlers to our methods
    1073          parser.StartElementHandler = self.StartElement
    1074          parser.EndElementHandler = self.EndElement
    1075          parser.CharacterDataHandler = self.CharacterData
    1076          # Parse the XML File
    1077          try:
    1078              parserStatus = parser.Parse(uiautomatorxml.encode(encoding='utf-8', errors='replace'), True)
    1079          except xml.parsers.expat.ExpatError, ex:
    1080              print >>sys.stderr, "ERROR: Offending XML:\n", repr(uiautomatorxml)
    1081              raise RuntimeError(ex)
    1082          return self.root
    1083
    1084 -class Excerpt2Code():
    1085      ''' Excerpt XML to code '''
    1086
    1087 -    def __init__(self):
    1088          self.data = None
    1089
    1090 -    def StartElement(self, name, attributes):
    1091          '''
    1092          Expat start element event handler
    1093          '''
    1094          if name == 'excerpt':
    1095              pass
    1096          else:
    1097              warnings.warn("Unexpected element: '%s'" % name)
    1098
    1099 -    def EndElement(self, name):
    1100          '''
    1101          Expat end element event handler
    1102          '''
    1103
    1104          if name == 'excerpt':
    1105              pass
    1106
    1107 -    def CharacterData(self, data):
    1108          '''
    1109          Expat character data event handler
    1110          '''
    1111
    1112          if data.strip():
    1113              data = data.encode()
    1114              if not self.data:
    1115                  self.data = data
    1116              else:
    1117                  self.data += data
    1118
    1119 -    def Parse(self, excerpt):
    1120          # Create an Expat parser
    1121          parser = xml.parsers.expat.ParserCreate()
    1122          # Set the Expat event handlers to our methods
    1123          parser.StartElementHandler = self.StartElement
    1124          parser.EndElementHandler = self.EndElement
    1125          parser.CharacterDataHandler = self.CharacterData
    1126          # Parse the XML
    1127          parserStatus = parser.Parse(excerpt, 1)
    1128          return self.data
    1129
    1130 -class ViewClient:
    1131      '''
    1132      ViewClient is a I{ViewServer} client.
    1133
    1134      ViewServer backend
    1135      ==================
    1136      If not running the ViewServer is started on the target device or emulator and then the port
    1137      mapping is created.
    1138
    1139      UiAutomator backend
    1140      ===================
    1141      No service is started.
    1142      '''
    1143
    1144 -    def __init__(self, device, serialno, adb=None, autodump=True, forceviewserveruse=False, localport=VIEW_SERVER_PORT, remoteport=VIEW_SERVER_PORT, startviewserver=True, ignoreuiautomatorkilled=False):
    1145          '''
    1146          Constructor
    1147
    1148          @type device: MonkeyDevice
    1149          @param device: The device running the C{View server} to which this client will connect
    1150          @type serialno: str
    1151          @param serialno: the serial number of the device or emulator to connect to
    1152          @type adb: str
    1153          @param adb: the path of the C{adb} executable or None and C{ViewClient} will try to find it
    1154          @type autodump: boolean
    1155          @param autodump: whether an automatic dump is performed at the end of this constructor
    1156          @type forceviewserveruse: boolean
    1157          @param forceviewserveruse: Force the use of C{ViewServer} even if the conditions to use
    1158                              C{UiAutomator} are satisfied
    1159          @type localport: int
    1160          @param localport: the local port used in the redirection
    1161          @type remoteport: int
    1162          @param remoteport: the remote port used to start the C{ViewServer} in the device or
    1163                             emulator
    1164          @type startviewserver: boolean
    1165          @param startviewserver: Whether to start the B{global} ViewServer
    1166          @type ignoreuiautomatorkilled: boolean
    1167          @param ignoreuiautomatorkilled: Ignores received B{Killed} message from C{uiautomator}
    1168          '''
    1169
    1170          if not device:
    1171              raise Exception('Device is not connected')
    1172          self.device = device
    1173          ''' The C{MonkeyDevice} device instance '''
    1174
    1175          if not serialno:
    1176              raise ValueError("Serialno cannot be None")
    1177          self.serialno = self.__mapSerialNo(serialno)
    1178          ''' The serial number of the device '''
    1179
    1180          if DEBUG_DEVICE: print >> sys.stderr, "ViewClient: using device with serialno", self.serialno
    1181
    1182          if adb:
    1183              if not os.access(adb, os.X_OK):
    1184                  raise Exception('adb="%s" is not executable' % adb)
    1185          else:
    1186              # Using adbclient we don't need adb executable yet (maybe it's needed if we want to
    1187              # start adb if not running)
    1188              adb = ViewClient.__obtainAdbPath()
    1189
    1190          self.adb = adb
    1191          ''' The adb command '''
    1192          self.root = None
    1193          ''' The root node '''
    1194          self.viewsById = {}
    1195          ''' The map containing all the L{View}s indexed by their L{View.getUniqueId()} '''
    1196          self.display = {}
    1197          ''' The map containing the device's display properties: width, height and density '''
    1198          for prop in [ 'width', 'height', 'density' ]:
    1199              self.display[prop] = -1
    1200              if USE_ADB_CLIENT_TO_GET_BUILD_PROPERTIES:
    1201                  try:
    1202                      self.display[prop] = int(device.getProperty('display.' + prop))
    1203                  except:
    1204                      if WARNINGS:
    1205                          warnings.warn("Couldn't determine display %s" % prop)
    1206              else:
    1207                  # these values are usually not defined as properties, so we stick to the -1 set
    1208                  # before
    1209                  pass
    1210
    1211          self.build = {}
    1212          ''' The map containing the device's build properties: version.sdk, version.release '''
    1213
    1214          for prop in [VERSION_SDK_PROPERTY, VERSION_RELEASE_PROPERTY]:
    1215              self.build[prop] = -1
    1216              try:
    1217                  if USE_ADB_CLIENT_TO_GET_BUILD_PROPERTIES:
    1218                      self.build[prop] = device.getProperty(prop)
    1219                  else:
    1220                      self.build[prop] = device.shell('getprop ro.build.' + prop)[:-2]
    1221              except:
    1222                  if WARNINGS:
    1223                      warnings.warn("Couldn't determine build %s" % prop)
    1224
    1225              if prop == VERSION_SDK_PROPERTY:
    1226                  # we expect it to be an int
    1227                  self.build[prop] = int(self.build[prop] if self.build[prop] else -1)
    1228
    1229          self.ro = {}
    1230          ''' The map containing the device's ro properties: secure, debuggable '''
    1231          for prop in ['secure', 'debuggable']:
    1232              try:
    1233                  self.ro[prop] = device.shell('getprop ro.' + prop)[:-2]
    1234              except:
    1235                  if WARNINGS:
    1236                      warnings.warn("Couldn't determine ro %s" % prop)
    1237                  self.ro[prop] = 'UNKNOWN'
    1238
    1239          self.forceViewServerUse = forceviewserveruse
    1240          ''' Force the use of ViewServer even if the conditions to use UiAutomator are satisfied '''
    1241          self.useUiAutomator = (self.build[VERSION_SDK_PROPERTY] >= 16) and not forceviewserveruse # jelly bean 4.1 & 4.2
    1242          if DEBUG:
    1243              print >> sys.stderr, "    ViewClient.__init__: useUiAutomator=", self.useUiAutomator, "sdk=", self.build[VERSION_SDK_PROPERTY], "forceviewserveruse=", forceviewserveruse
    1244          ''' If UIAutomator is supported by the device it will be used '''
    1245          self.ignoreUiAutomatorKilled = ignoreuiautomatorkilled
    1246          ''' On some devices (i.e. Nexus 7 running 4.2.2) uiautomator is killed just after generating
    1247          the dump file. In many cases the file is already complete so we can ask to ignore the 'Killed'
    1248          message by setting L{ignoreuiautomatorkilled} to C{True}.
    1249
    1250          Changes in v2.3.21 that uses C{/dev/tty} instead of a file may have turned this variable
    1251          unnecessary, however it has been kept for backward compatibility.
    1252          '''
    1253
    1254          if self.useUiAutomator:
    1255              self.textProperty = TEXT_PROPERTY_UI_AUTOMATOR
    1256          else:
    1257              if self.build[VERSION_SDK_PROPERTY] <= 10:
    1258                  self.textProperty = TEXT_PROPERTY_API_10
    1259              else:
    1260                  self.textProperty = TEXT_PROPERTY
    1261              if startviewserver:
    1262                  if not self.serviceResponse(device.shell('service call window 3')):
    1263                      try:
    1264                          self.assertServiceResponse(device.shell('service call window 1 i32 %d' %
    1265                                                          remoteport))
    1266                      except:
    1267                          msg = 'Cannot start View server.\n' \
    1268                              'This only works on emulator and devices running developer versions.\n' \
    1269                              'Does hierarchyviewer work on your device?\n' \
    1270                              'See https://github.com/dtmilano/AndroidViewClient/wiki/Secure-mode\n\n' \
    1271                              'Device properties:\n' \
    1272                              '    ro.secure=%s\n' \
    1273                              '    ro.debuggable=%s\n' % (self.ro['secure'], self.ro['debuggable'])
    1274                          raise Exception(msg)
    1275
    1276              self.localPort = localport
    1277              self.remotePort = remoteport
    1278              # FIXME: it seems there's no way of obtaining the serialno from the MonkeyDevice
    1279              subprocess.check_call([self.adb, '-s', self.serialno, 'forward', 'tcp:%d' % self.localPort,
    1280                                      'tcp:%d' % self.remotePort])
    1281
    1282          self.windows = None
    1283          ''' The list of windows as obtained by L{ViewClient.list()} '''
    1284
    1285          if autodump:
    1286              self.dump()
    1287
    1288 -    def __del__(self):
    1289          # should clean up some things
    1290          pass
    1291
    1292      @staticmethod
    1293 -    def __obtainAdbPath():
    1294          '''
    1295          Obtains the ADB path attempting know locations for different OSs
    1296          '''
    1297
    1298          osName = platform.system()
    1299          isWindows = False
    1300          if osName.startswith('Windows'):
    1301              adb = 'adb.exe'
    1302              isWindows = True
    1303          else:
    1304              adb = 'adb'
    1305
    1306          ANDROID_HOME = os.environ['ANDROID_HOME'] if os.environ.has_key('ANDROID_HOME') else '/opt/android-sdk'
    1307          HOME = os.environ['HOME'] if os.environ.has_key('HOME') else ''
    1308
    1309          possibleChoices = [ os.path.join(ANDROID_HOME, 'platform-tools', adb),
    1310                             os.path.join(HOME,  "android", 'platform-tools', adb),
    1311                             os.path.join(HOME,  "android-sdk", 'platform-tools', adb),
    1312                             adb,
    1313                             ]
    1314
    1315          if osName.startswith('Windows'):
    1316              possibleChoices.append(os.path.join("""C:\Program Files\Android\android-sdk\platform-tools""", adb))
    1317              possibleChoices.append(os.path.join("""C:\Program Files (x86)\Android\android-sdk\platform-tools""", adb))
    1318          elif osName.startswith('Linux'):
    1319              possibleChoices.append(os.path.join("opt", "android-sdk-linux",  'platform-tools', adb))
    1320              possibleChoices.append(os.path.join(HOME,  "opt", "android-sdk-linux",  'platform-tools', adb))
    1321              possibleChoices.append(os.path.join(HOME,  "android-sdk-linux",  'platform-tools', adb))
    1322          elif osName.startswith('Mac'):
    1323              possibleChoices.append(os.path.join("opt", "android-sdk-mac_x86",  'platform-tools', adb))
    1324              possibleChoices.append(os.path.join(HOME,  "opt", "android-sdk-mac", 'platform-tools', adb))
    1325              possibleChoices.append(os.path.join(HOME,  "android-sdk-mac", 'platform-tools', adb))
    1326              possibleChoices.append(os.path.join(HOME,  "opt", "android-sdk-mac_x86",  'platform-tools', adb))
    1327              possibleChoices.append(os.path.join(HOME,  "android-sdk-mac_x86",  'platform-tools', adb))
    1328          else:
    1329              # Unsupported OS
    1330              pass
    1331
    1332          for exeFile in possibleChoices:
    1333              if os.access(exeFile, os.X_OK):
    1334                  return exeFile
    1335
    1336          for path in os.environ["PATH"].split(os.pathsep):
    1337              exeFile = os.path.join(path, adb)
    1338              if exeFile != None and os.access(exeFile, os.X_OK if not isWindows else os.F_OK):
    1339                  return exeFile
    1340
    1341          raise Exception('adb="%s" is not executable. Did you forget to set ANDROID_HOME in the environment?' % adb)
    1342
    1343      @staticmethod
    1344 -    def __mapSerialNo(serialno):
    1345          serialno = serialno.strip()
    1346          #ipRE = re.compile('^\d+\.\d+.\d+.\d+$')
    1347          if IP_RE.match(serialno):
    1348              if DEBUG_DEVICE: print >>sys.stderr, "ViewClient: adding default port to serialno", serialno, ADB_DEFAULT_PORT
    1349              return serialno + ':%d' % ADB_DEFAULT_PORT
    1350
    1351          ipPortRE = re.compile('^\d+\.\d+.\d+.\d+:\d+$')
    1352          if ipPortRE.match(serialno):
    1353              # nothing to map
    1354              return serialno
    1355
    1356          if re.search("[.*()+]", serialno):
    1357              raise ValueError("Regular expression not supported as serialno in ViewClient")
    1358
    1359          return serialno
    1360
    1361      @staticmethod
    1362 -    def __obtainDeviceSerialNumber(device):
    1363          if DEBUG_DEVICE: print >>sys.stderr, "ViewClient: obtaining serial number for connected device"
    1364          serialno = device.getProperty('ro.serialno')
    1365          if not serialno:
    1366              serialno = device.shell('getprop ro.serialno')
    1367              if serialno:
    1368                  serialno = serialno[:-2]
    1369          if not serialno:
    1370              qemu = device.shell('getprop ro.kernel.qemu')
    1371              if qemu:
    1372                  qemu = qemu[:-2]
    1373                  if qemu and int(qemu) == 1:
    1374                      # FIXME !!!!!
    1375                      # this must be calculated from somewhere, though using a fixed serialno for now
    1376                      warnings.warn("Running on emulator but no serial number was specified then 'emulator-5554' is used")
    1377                      serialno = 'emulator-5554'
    1378          if not serialno:
    1379              # If there's only one device connected get its serialno
    1380              adb = ViewClient.__obtainAdbPath()
    1381              if DEBUG_DEVICE: print >>sys.stderr, "    using adb=%s" % adb
    1382              s = subprocess.Popen([adb, 'get-serialno'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={}).communicate()[0][:-1]
    1383              if s != 'unknown':
    1384                  serialno = s
    1385          if DEBUG_DEVICE: print >>sys.stderr, "    serialno=%s" % serialno
    1386          if not serialno:
    1387              warnings.warn("Couldn't obtain the serialno of the connected device")
    1388          return serialno
    1389
    1390      @staticmethod
    1391 -    def setAlarm(timeout):
    1392          osName = platform.system()
    1393          if osName.startswith('Windows'): # alarm is not implemented in Windows
    1394              return
    1395          signal.alarm(timeout)
    1396
    1397      @staticmethod
    1398 -    def connectToDeviceOrExit(timeout=60, verbose=False, ignoresecuredevice=False, serialno=None):
    1399          '''
    1400          Connects to a device which serial number is obtained from the script arguments if available
    1401          or using the default regex C{.*}.
    1402
    1403          If the connection is not successful the script exits.
    1404          L{MonkeyRunner.waitForConnection()} returns a L{MonkeyDevice} even if the connection failed.
    1405          Then, to detect this situation, C{device.wake()} is attempted and if it fails then it is
    1406          assumed the previous connection failed.
    1407
    1408          @type timeout: int
    1409          @param timeout: timeout for the connection
    1410          @type verbose: bool
    1411          @param verbose: Verbose output
    1412          @type ignoresecuredevice: bool
    1413          @param ignoresecuredevice: Ignores the check for a secure device
    1414          @type serialno: str
    1415          @param serialno: The device or emulator serial number
    1416
    1417          @return: the device and serialno used for the connection
    1418          '''
    1419
    1420          progname = os.path.basename(sys.argv[0])
    1421          if serialno is None:
    1422              # eat all the extra options the invoking script may have added
    1423              args = sys.argv
    1424              while len(args) > 1 and args[1][0] == '-':
    1425                  args.pop(1)
    1426              serialno = args[1] if len(args) > 1 else \
    1427                      os.environ['ANDROID_SERIAL'] if os.environ.has_key('ANDROID_SERIAL') \
    1428                      else '.*'
    1429          if IP_RE.match(serialno):
    1430              # If matches an IP address format and port was not specified add the default
    1431              serialno += ':%d' % ADB_DEFAULT_PORT
    1432          if verbose:
    1433              print >> sys.stderr, 'Connecting to a device with serialno=%s with a timeout of %d secs...' % \
    1434                  (serialno, timeout)
    1435          ViewClient.setAlarm(timeout+5)
    1436          device = adbclient.AdbClient(serialno)
    1437          ViewClient.setAlarm(0)
    1438          if verbose:
    1439              print >> sys.stderr, 'Connected to device with serialno=%s' % serialno
    1440          secure = device.getSystemProperty('ro.secure')
    1441          debuggable = device.getSystemProperty('ro.debuggable')
    1442          versionProperty = device.getProperty(VERSION_SDK_PROPERTY)
    1443          if versionProperty:
    1444              version = int(versionProperty)
    1445          else:
    1446              if verbose:
    1447                  print "Couldn't obtain device SDK version"
    1448              version = -1
    1449
    1450          # we are going to use UiAutomator for versions >= 16 that's why we ignore if the device
    1451          # is secure if this is true
    1452          if secure == '1' and debuggable == '0' and not ignoresecuredevice and version < 16:
    1453              print >> sys.stderr, "%s: ERROR: Device is secure, AndroidViewClient won't work." % progname
    1454              if verbose:
    1455                  print >> sys.stderr, "    secure=%s debuggable=%s version=%d ignoresecuredevice=%s" % \
    1456                      (secure, debuggable, version, ignoresecuredevice)
    1457              sys.exit(2)
    1458          if re.search("[.*()+]", serialno) and not re.search("(\d{1,3}\.){3}\d{1,3}", serialno):
    1459              # if a regex was used we have to determine the serialno used
    1460              serialno = ViewClient.__obtainDeviceSerialNumber(device)
    1461          if verbose:
    1462              print >> sys.stderr, 'Actual device serialno=%s' % serialno
    1463          return device, serialno
    1464
    1465      @staticmethod
    1466 -    def traverseShowClassIdAndText(view, extraInfo=None, noextrainfo=None):
    1467          '''
    1468          Shows the View class, id and text if available.
    1469          This function can be used as a transform function to L{ViewClient.traverse()}
    1470
    1471          @type view: I{View}
    1472          @param view: the View
    1473          @type extraInfo: method
    1474          @param extraInfo: the View method to add extra info
    1475          @type noextrainfo: bool
    1476          @param noextrainfo: Don't add extra info
    1477
    1478          @return: the string containing class, id, and text if available
    1479          '''
    1480
    1481          try:
    1482              eis = ''
    1483              if extraInfo:
    1484                  eis = extraInfo(view).__str__()
    1485                  if not eis and noextrainfo:
    1486                      eis = noextrainfo
    1487              if eis:
    1488                  eis = ' ' + eis
    1489              return "%s %s %s%s" % (view.getClass(), view.getId(), view.getText(), eis)
    1490          except Exception, e:
    1491              return "Exception in view=%s: %s" % (view.__smallStr__(), e)
    1492
    1493      @staticmethod
    1494 -    def traverseShowClassIdTextAndUniqueId(view):
    1495          '''
    1496          Shows the View class, id, text if available and unique id.
    1497          This function can be used as a transform function to L{ViewClient.traverse()}
    1498
    1499          @type view: I{View}
    1500          @param view: the View
    1501          @return: the string containing class, id, and text if available and unique Id
    1502          '''
    1503
    1504          return ViewClient.traverseShowClassIdAndText(view, View.getUniqueId)
    1505
    1506      @staticmethod
    1507 -    def traverseShowClassIdTextAndContentDescription(view):
    1508          '''
    1509          Shows the View class, id, text if available and unique id.
    1510          This function can be used as a transform function to L{ViewClient.traverse()}
    1511
    1512          @type view: I{View}
    1513          @param view: the View
    1514          @return: the string containing class, id, and text if available and the content description
    1515          '''
    1516
    1517          return ViewClient.traverseShowClassIdAndText(view, View.getContentDescription, 'NAF')
    1518
    1519      @staticmethod
    1520 -    def traverseShowClassIdTextAndCenter(view):
    1521          '''
    1522          Shows the View class, id and text if available.
    1523          This function can be used as a transform function to L{ViewClient.traverse()}
    1524
    1525          @type view: I{View}
    1526          @param view: the View
    1527          @return: the string containing class, id, and text if available
    1528          '''
    1529
    1530          return ViewClient.traverseShowClassIdAndText(view, View.getCenter)
    1531
    1532      @staticmethod
    1533 -    def traverseShowClassIdTextPositionAndSize(view):
    1534          '''
    1535          Shows the View class, id and text if available.
    1536          This function can be used as a transform function to L{ViewClient.traverse()}
    1537
    1538          @type view: I{View}
    1539          @param view: the View
    1540          @return: the string containing class, id, and text if available
    1541          '''
    1542
    1543          return ViewClient.traverseShowClassIdAndText(view, View.getPositionAndSize)
    1544
    1545      # methods that can be used to transform ViewClient.traverse output
    1546      TRAVERSE_CIT = traverseShowClassIdAndText
    1547      ''' An alias for L{traverseShowClassIdAndText(view)} '''
    1548      TRAVERSE_CITUI = traverseShowClassIdTextAndUniqueId
    1549      ''' An alias for L{traverseShowClassIdTextAndUniqueId(view)} '''
    1550      TRAVERSE_CITCD = traverseShowClassIdTextAndContentDescription
    1551      ''' An alias for L{traverseShowClassIdTextAndContentDescription(view)} '''
    1552      TRAVERSE_CITC = traverseShowClassIdTextAndCenter
    1553      ''' An alias for L{traverseShowClassIdTextAndCenter(view)} '''
    1554      TRAVERSE_CITPS = traverseShowClassIdTextPositionAndSize
    1555      ''' An alias for L{traverseShowClassIdTextPositionAndSize(view)} '''
    1556
    1557      @staticmethod
    1558 -    def sleep(secs=1.0):
    1559          '''
    1560          Sleeps for the specified number of seconds.
    1561
    1562          @type secs: float
    1563          @param secs: number of seconds
    1564          '''
    1565          time.sleep(secs)
    1566
    1567 -    def assertServiceResponse(self, response):
    1568          '''
    1569          Checks whether the response received from the server is correct or raises and Exception.
    1570
    1571          @type response: str
    1572          @param response: Response received from the server
    1573
    1574          @raise Exception: If the response received from the server is invalid
    1575          '''
    1576
    1577          if not self.serviceResponse(response):
    1578              raise Exception('Invalid response received from service.')
    1579
    1580 -    def serviceResponse(self, response):
    1581          '''
    1582          Checks the response received from the I{ViewServer}.
    1583
    1584          @return: C{True} if the response received matches L{PARCEL_TRUE}, C{False} otherwise
    1585          '''
    1586
    1587          PARCEL_TRUE = "Result: Parcel(00000000 00000001   '........')\r\n"
    1588          ''' The TRUE response parcel '''
    1589          if DEBUG:
    1590              print >>sys.stderr, "serviceResponse: comparing '%s' vs Parcel(%s)" % (response, PARCEL_TRUE)
    1591          return response == PARCEL_TRUE
    1592
    1593 -    def setViews(self, received):
    1594          '''
    1595          Sets L{self.views} to the received value splitting it into lines.
    1596
    1597          @type received: str
    1598          @param received: the string received from the I{View Server}
    1599          '''
    1600
    1601          if not received or received == "":
    1602              raise ValueError("received is empty")
    1603          self.views = []
    1604          ''' The list of Views represented as C{str} obtained after splitting it into lines after being received from the server. Done by L{self.setViews()}. '''
    1605          self.__parseTree(received.split("\n"))
    1606          if DEBUG:
    1607              print >>sys.stderr, "there are %d views in this dump" % len(self.views)
    1608
    1609 -    def setViewsFromUiAutomatorDump(self, received):
    1610          '''
    1611          Sets L{self.views} to the received value parsing the received XML.
    1612
    1613          @type received: str
    1614          @param received: the string received from the I{UI Automator}
    1615          '''
    1616
    1617          if not received or received == "":
    1618              raise ValueError("received is empty")
    1619          self.views = []
    1620          ''' The list of Views represented as C{str} obtained after splitting it into lines after being received from the server. Done by L{self.setViews()}. '''
    1621          self.__parseTreeFromUiAutomatorDump(received)
    1622          if DEBUG:
    1623              print >>sys.stderr, "there are %d views in this dump" % len(self.views)
    1624
    1625
    1626 -    def __splitAttrs(self, strArgs):
    1627          '''
    1628          Splits the C{View} attributes in C{strArgs} and optionally adds the view id to the C{viewsById} list.
    1629
    1630          Unique Ids
    1631          ==========
    1632          It is very common to find C{View}s having B{NO_ID} as the Id. This turns very difficult to
    1633          use L{self.findViewById()}. To help in this situation this method assigns B{unique Ids}.
    1634
    1635          The B{unique Ids} are generated using the pattern C{id/no_id/<number>} with C{<number>} starting
    1636          at 1.
    1637
    1638          @type strArgs: str
    1639          @param strArgs: the string containing the raw list of attributes and values
    1640
    1641          @return: Returns the attributes map.
    1642          '''
    1643
    1644          if self.useUiAutomator:
    1645              raise RuntimeError("This method is not compatible with UIAutomator")
    1646          # replace the spaces in text:mText to preserve them in later split
    1647          # they are translated back after the attribute matches
    1648          textRE = re.compile('%s=%s,' % (self.textProperty, _nd('len')))
    1649          m = textRE.search(strArgs)
    1650          if m:
    1651              __textStart = m.end()
    1652              __textLen = int(m.group('len'))
    1653              __textEnd = m.end() + __textLen
    1654              s1 = strArgs[__textStart:__textEnd]
    1655              s2 = s1.replace(' ', WS)
    1656              strArgs = strArgs.replace(s1, s2, 1)
    1657
    1658          idRE = re.compile("(?P<viewId>id/\S+)")
    1659          attrRE = re.compile('%s(?P<parens>\(\))?=%s,(?P<val>[^ ]*)' % (_ns('attr'), _nd('len')), flags=re.DOTALL)
    1660          hashRE = re.compile('%s@%s' % (_ns('class'), _nh('oid')))
    1661
    1662          attrs = {}
    1663          viewId = None
    1664          m = idRE.search(strArgs)
    1665          if m:
    1666              viewId = m.group('viewId')
    1667              if DEBUG:
    1668                  print >>sys.stderr, "found view with id=%s" % viewId
    1669
    1670          for attr in strArgs.split():
    1671              m = attrRE.match(attr)
    1672              if m:
    1673                  __attr = m.group('attr')
    1674                  __parens = '()' if m.group('parens') else ''
    1675                  __len = int(m.group('len'))
    1676                  __val = m.group('val')
    1677                  if WARNINGS and __len != len(__val):
    1678                      warnings.warn("Invalid len: expected: %d   found: %d   s=%s   e=%s" % (__len, len(__val), __val[:50], __val[-50:]))
    1679                  if __attr == self.textProperty:
    1680                      # restore spaces that have been replaced
    1681                      __val = __val.replace(WS, ' ')
    1682                  attrs[__attr + __parens] = __val
    1683              else:
    1684                  m = hashRE.match(attr)
    1685                  if m:
    1686                      attrs['class'] = m.group('class')
    1687                      attrs['oid'] = m.group('oid')
    1688                  else:
    1689                      if DEBUG:
    1690                          print >>sys.stderr, attr, "doesn't match"
    1691
    1692          if True: # was assignViewById
    1693              if not viewId:
    1694                  # If the view has NO_ID we are assigning a default id here (id/no_id) which is
    1695                  # immediately incremented if another view with no id was found before to generate
    1696                  # a unique id
    1697                  viewId = "id/no_id/1"
    1698              if viewId in self.viewsById:
    1699                  # sometimes the view ids are not unique, so let's generate a unique id here
    1700                  i = 1
    1701                  while True:
    1702                      newId = re.sub('/\d+$', '', viewId) + '/%d' % i
    1703                      if not newId in self.viewsById:
    1704                          break
    1705                      i += 1
    1706                  viewId = newId
    1707                  if DEBUG:
    1708                      print >>sys.stderr, "adding viewById %s" % viewId
    1709              # We are assigning a new attribute to keep the original id preserved, which could have
    1710              # been NO_ID repeated multiple times
    1711              attrs['uniqueId'] = viewId
    1712
    1713          return attrs
    1714
    1715 -    def __parseTree(self, receivedLines):
    1716          '''
    1717          Parses the View tree contained in L{receivedLines}. The tree is created and the root node assigned to L{self.root}.
    1718          This method also assigns L{self.viewsById} values using L{View.getUniqueId} as the key.
    1719
    1720          @type receivedLines: str
    1721          @param receivedLines: the string received from B{View Server}
    1722          '''
    1723
    1724          self.root = None
    1725          self.viewsById = {}
    1726          self.views = []
    1727          parent = None
    1728          parents = []
    1729          treeLevel = -1
    1730          newLevel = -1
    1731          lastView = None
    1732          for v in receivedLines:
    1733              if v == '' or v == 'DONE' or v == 'DONE.':
    1734                  break
    1735              attrs = self.__splitAttrs(v)
    1736              if not self.root:
    1737                  if v[0] == ' ':
    1738                      raise Exception("Unexpected root element starting with ' '.")
    1739                  self.root = View.factory(attrs, self.device, self.build[VERSION_SDK_PROPERTY], self.forceViewServerUse)
    1740                  if DEBUG: self.root.raw = v
    1741                  treeLevel = 0
    1742                  newLevel = 0
    1743                  lastView = self.root
    1744                  parent = self.root
    1745                  parents.append(parent)
    1746              else:
    1747                  newLevel = (len(v) - len(v.lstrip()))
    1748                  if newLevel == 0:
    1749                      raise Exception("newLevel==0 treeLevel=%d but tree can have only one root, v=%s" % (treeLevel, v))
    1750                  child = View.factory(attrs, self.device, self.build[VERSION_SDK_PROPERTY], self.forceViewServerUse)
    1751                  if DEBUG: child.raw = v
    1752                  if newLevel == treeLevel:
    1753                      parent.add(child)
    1754                      lastView = child
    1755                  elif newLevel > treeLevel:
    1756                      if (newLevel - treeLevel) != 1:
    1757                          raise Exception("newLevel jumps %d levels, v=%s" % ((newLevel-treeLevel), v))
    1758                      parent = lastView
    1759                      parents.append(parent)
    1760                      parent.add(child)
    1761                      lastView = child
    1762                      treeLevel = newLevel
    1763                  else: # newLevel < treeLevel
    1764                      for i in range(treeLevel - newLevel):
    1765                          parents.pop()
    1766                      parent = parents.pop()
    1767                      parents.append(parent)
    1768                      parent.add(child)
    1769                      treeLevel = newLevel
    1770                      lastView = child
    1771              self.views.append(lastView)
    1772              self.viewsById[lastView.getUniqueId()] = lastView
    1773
    1774 -    def __parseTreeFromUiAutomatorDump(self, receivedXml):
    1775          parser = UiAutomator2AndroidViewClient(self.device, self.build[VERSION_SDK_PROPERTY])
    1776          self.root = parser.Parse(receivedXml)
    1777          self.views = parser.views
    1778          self.viewsById = {}
    1779          for v in self.views:
    1780              self.viewsById[v.getUniqueId()] = v
    1781
    1782 -    def getRoot(self):
    1783          '''
    1784          Gets the root node of the C{View} tree
    1785
    1786          @return: the root node of the C{View} tree
    1787          '''
    1788          return self.root
    1789
    1790 -    def traverse(self, root="ROOT", indent="", transform=View.__str__, stream=sys.stdout):
    1791          '''
    1792          Traverses the C{View} tree and prints its nodes.
    1793
    1794          The nodes are printed converting them to string but other transformations can be specified
    1795          by providing a method name as the C{transform} parameter.
    1796
    1797          @type root: L{View}
    1798          @param root: the root node from where the traverse starts
    1799          @type indent: str
    1800          @param indent: the indentation string to use to print the nodes
    1801          @type transform: method
    1802          @param transform: a method to use to transform the node before is printed
    1803          '''
    1804
    1805          if type(root) == types.StringType and root == "ROOT":
    1806              root = self.root
    1807
    1808          return ViewClient.__traverse(root, indent, transform, stream)
    1809  #         if not root:
    1810  #             return
    1811  #
    1812  #         s = transform(root)
    1813  #         if s:
    1814  #             print >>stream, "%s%s" % (indent, s)
    1815  #
    1816  #         for ch in root.children:
    1817  #             self.traverse(ch, indent=indent+"   ", transform=transform, stream=stream)
    1818
    1819      @staticmethod
    1820 -    def __traverse(root, indent="", transform=View.__str__, stream=sys.stdout):
    1821          if not root:
    1822              return
    1823
    1824          s = transform(root)
    1825          if s:
    1826              ius = "%s%s" % (indent, s if isinstance(s, unicode) else unicode(s, 'utf-8', 'replace'))
    1827              print >>stream, ius.encode('utf-8', 'replace')
    1828
    1829          for ch in root.children:
    1830              ViewClient.__traverse(ch, indent=indent+"   ", transform=transform, stream=stream)
    1831
    1832 -    def dump(self, window=-1, sleep=1):
    1833          '''
    1834          Dumps the window content.
    1835
    1836          Sleep is useful to wait some time before obtaining the new content when something in the
    1837          window has changed.
    1838
    1839          @type window: int or str
    1840          @param window: the window id or name of the window to dump.
    1841                      The B{name} is the package name or the window name (i.e. StatusBar) for
    1842                      system windows.
    1843                      The window id can be provided as C{int} or C{str}. The C{str} should represent
    1844                      and C{int} in either base 10 or 16.
    1845                      Use -1 to dump all windows.
    1846                      This parameter only is used when the backend is B{ViewServer} and it's
    1847                      ignored for B{UiAutomator}.
    1848          @type sleep: int
    1849          @param sleep: sleep in seconds before proceeding to dump the content
    1850
    1851          @return: the list of Views as C{str} received from the server after being split into lines
    1852          '''
    1853
    1854          if sleep > 0:
    1855              time.sleep(sleep)
    1856
    1857          if self.useUiAutomator:
    1858              # NOTICE:
    1859              # Using /dev/tty this works even on devices with no sdcard
    1860              received = unicode(self.device.shell('uiautomator dump /dev/tty >/dev/null'), encoding='utf-8', errors='replace')
    1861              if not received:
    1862                  raise RuntimeError('ERROR: Empty UiAutomator dump was received')
    1863              if DEBUG:
    1864                  self.received = received
    1865              if DEBUG_RECEIVED:
    1866                  print >>sys.stderr, "received %d chars" % len(received)
    1867                  print >>sys.stderr
    1868                  print >>sys.stderr, repr(received)
    1869                  print >>sys.stderr
    1870              onlyKilledRE = re.compile('[\n\S]*Killed[\n\r\S]*', re.MULTILINE)
    1871              if onlyKilledRE.search(received):
    1872                  raise RuntimeError('''ERROR: UiAutomator output contains no valid information. UiAutomator was killed, no reason given.''')
    1873              if self.ignoreUiAutomatorKilled:
    1874                  if DEBUG_RECEIVED:
    1875                      print >>sys.stderr, "ignoring UiAutomator Killed"
    1876                  killedRE = re.compile('</hierarchy>[\n\S]*Killed', re.MULTILINE)
    1877                  if killedRE.search(received):
    1878                      received = re.sub(killedRE, '</hierarchy>', received)
    1879                  elif DEBUG_RECEIVED:
    1880                      print "UiAutomator Killed: NOT FOUND!"
    1881                  # It seems that API18 uiautomator spits this message to stdout
    1882                  dumpedToDevTtyRE = re.compile('</hierarchy>[\n\S]*UI hierchary dumped to: /dev/tty.*', re.MULTILINE)
    1883                  if dumpedToDevTtyRE.search(received):
    1884                      received = re.sub(dumpedToDevTtyRE, '</hierarchy>', received)
    1885                  # API19 seems to send this warning as part of the XML.
    1886                  # Let's remove it if present
    1887                  received = received.replace('WARNING: linker: libdvm.so has text relocations. This is wasting memory and is a security risk. Please fix.\r\n', '')
    1888                  if DEBUG_RECEIVED:
    1889                      print >>sys.stderr, "received=", received
    1890              if re.search('\[: not found', received):
    1891                  raise RuntimeError('''ERROR: Some emulator images (i.e. android 4.1.2 API 16 generic_x86) does not include the '[' command.
    1892  While UiAutomator back-end might be supported 'uiautomator' command fails.
    1893  You should force ViewServer back-end.''')
    1894              self.setViewsFromUiAutomatorDump(received)
    1895          else:
    1896              if isinstance(window, str):
    1897                  if window != '-1':
    1898                      self.list(sleep=0)
    1899                      found = False
    1900                      for wId in self.windows:
    1901                          try:
    1902                              if window == self.windows[wId]:
    1903                                  window = wId
    1904                                  found = True
    1905                                  break
    1906                          except:
    1907                              pass
    1908                          try:
    1909                              if int(window) == wId:
    1910                                  window = wId
    1911                                  found = True
    1912                                  break
    1913                          except:
    1914                              pass
    1915                          try:
    1916                              if int(window, 16) == wId:
    1917                                  window = wId
    1918                                  found = True
    1919                                  break
    1920                          except:
    1921                              pass
    1922
    1923                      if not found:
    1924                          raise RuntimeError("ERROR: Cannot find window '%s' in %s" % (window, self.windows))
    1925                  else:
    1926                      window = -1
    1927
    1928              s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    1929              try:
    1930                  s.connect((VIEW_SERVER_HOST, self.localPort))
    1931              except socket.error, ex:
    1932                  raise RuntimeError("ERROR: Connecting to %s:%d: %s" % (VIEW_SERVER_HOST, self.localPort, ex))
    1933              cmd = 'dump %x\r\n' % window
    1934              if DEBUG:
    1935                  print >>sys.stderr, "executing: '%s'" % cmd
    1936              s.send(cmd)
    1937              received = ""
    1938              doneRE = re.compile("DONE")
    1939              ViewClient.setAlarm(120)
    1940              while True:
    1941                  if DEBUG_RECEIVED:
    1942                      print >>sys.stderr, "    reading from socket..."
    1943                  received += s.recv(1024)
    1944                  if doneRE.search(received[-7:]):
    1945                      break
    1946              s.close()
    1947              ViewClient.setAlarm(0)
    1948              if DEBUG:
    1949                  self.received = received
    1950              if DEBUG_RECEIVED:
    1951                  print >>sys.stderr, "received %d chars" % len(received)
    1952                  print >>sys.stderr
    1953                  print >>sys.stderr, received
    1954                  print >>sys.stderr
    1955              self.setViews(received)
    1956
    1957              if DEBUG_TREE:
    1958                  self.traverse(self.root)
    1959
    1960          return self.views
    1961
    1962 -    def list(self, sleep=1):
    1963          '''
    1964          List the windows.
    1965
    1966          Sleep is useful to wait some time before obtaining the new content when something in the
    1967          window has changed.
    1968          This also sets L{self.windows} as the list of windows.
    1969
    1970          @type sleep: int
    1971          @param sleep: sleep in seconds before proceeding to dump the content
    1972
    1973          @return: the list of windows
    1974          '''
    1975
    1976          if sleep > 0:
    1977              time.sleep(sleep)
    1978
    1979          if self.useUiAutomator:
    1980              raise Exception("Not implemented yet: listing windows with UiAutomator")
    1981          else:
    1982              s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    1983              try:
    1984                  s.connect((VIEW_SERVER_HOST, self.localPort))
    1985              except socket.error, ex:
    1986                  raise RuntimeError("ERROR: Connecting to %s:%d: %s" % (VIEW_SERVER_HOST, self.localPort, ex))
    1987              s.send('list\r\n')
    1988              received = ""
    1989              doneRE = re.compile("DONE")
    1990              while True:
    1991                  received += s.recv(1024)
    1992                  if doneRE.search(received[-7:]):
    1993                      break
    1994              s.close()
    1995              if DEBUG:
    1996                  self.received = received
    1997              if DEBUG_RECEIVED:
    1998                  print >>sys.stderr, "received %d chars" % len(received)
    1999                  print >>sys.stderr
    2000                  print >>sys.stderr, received
    2001                  print >>sys.stderr
    2002
    2003              self.windows = {}
    2004              for line in received.split('\n'):
    2005                  if not line:
    2006                      break
    2007                  if doneRE.search(line):
    2008                      break
    2009                  values = line.split()
    2010                  if len(values) > 1:
    2011                      package = values[1]
    2012                  else:
    2013                      package = "UNKNOWN"
    2014                  if len(values) > 0:
    2015                      wid = values[0]
    2016                  else:
    2017                      wid = '00000000'
    2018                  self.windows[int('0x' + wid, 16)] = package
    2019              return self.windows
    2020
    2021
    2022 -    def findViewById(self, viewId, root="ROOT", viewFilter=None):
    2023          '''
    2024          Finds the View with the specified viewId.
    2025
    2026          @type viewId: str
    2027          @param viewId: the ID of the view to find
    2028          @type root: str
    2029          @type root: View
    2030          @param root: the root node of the tree where the View will be searched
    2031          @type: viewFilter: function
    2032          @param viewFilter: a function that will be invoked providing the candidate View as a parameter
    2033                             and depending on the return value (C{True} or C{False}) the View will be
    2034                             selected and returned as the result of C{findViewById()} or ignored.
    2035                             This can be C{None} and no extra filtering is applied.
    2036
    2037          @return: the C{View} found or C{None}
    2038          '''
    2039
    2040          if not root:
    2041              return None
    2042
    2043          if type(root) == types.StringType and root == "ROOT":
    2044              return self.findViewById(viewId, self.root, viewFilter)
    2045
    2046          if root.getId() == viewId:
    2047              if viewFilter:
    2048                  if viewFilter(root):
    2049                      return root
    2050              else:
    2051                  return root
    2052
    2053          if re.match('^id/no_id', viewId) or re.match('^id/.+/.+', viewId):
    2054              if root.getUniqueId() == viewId:
    2055                  if viewFilter:
    2056                      if viewFilter(root):
    2057                          return root;
    2058                  else:
    2059                      return root
    2060
    2061
    2062          for ch in root.children:
    2063              foundView = self.findViewById(viewId, ch, viewFilter)
    2064              if foundView:
    2065                  if viewFilter:
    2066                      if viewFilter(foundView):
    2067                          return foundView
    2068                  else:
    2069                      return foundView
    2070
    2071 -    def findViewByIdOrRaise(self, viewId, root="ROOT", viewFilter=None):
    2072          '''
    2073          Finds the View or raise a ViewNotFoundException.
    2074
    2075          @type viewId: str
    2076          @param viewId: the ID of the view to find
    2077          @type root: str
    2078          @type root: View
    2079          @param root: the root node of the tree where the View will be searched
    2080          @type: viewFilter: function
    2081          @param viewFilter: a function that will be invoked providing the candidate View as a parameter
    2082                             and depending on the return value (C{True} or C{False}) the View will be
    2083                             selected and returned as the result of C{findViewById()} or ignored.
    2084                             This can be C{None} and no extra filtering is applied.
    2085          @return: the View found
    2086          @raise ViewNotFoundException: raise the exception if View not found
    2087          '''
    2088
    2089          view = self.findViewById(viewId, root, viewFilter)
    2090          if view:
    2091              return view
    2092          else:
    2093              raise ViewNotFoundException("ID", viewId, root)
    2094
    2095 -    def findViewByTag(self, tag, root="ROOT"):
    2096          '''
    2097          Finds the View with the specified tag
    2098          '''
    2099
    2100          return self.findViewWithAttribute('getTag()', tag, root)
    2101
    2102 -    def findViewByTagOrRaise(self, tag, root="ROOT"):
    2103          '''
    2104          Finds the View with the specified tag or raise a ViewNotFoundException
    2105          '''
    2106
    2107          view = self.findViewWithAttribute('getTag()', tag, root)
    2108          if view:
    2109              return view
    2110          else:
    2111              raise ViewNotFoundException("tag", tag, root)
    2112
    2113 -    def __findViewWithAttributeInTree(self, attr, val, root):
    2114          if not self.root:
    2115              print >>sys.stderr, "ERROR: no root, did you forget to call dump()?"
    2116              return None
    2117
    2118          if type(root) == types.StringType and root == "ROOT":
    2119              root = self.root
    2120
    2121          if DEBUG: print >>sys.stderr, "__findViewWithAttributeInTree: type val=", type(val)
    2122          if DEBUG: print >>sys.stderr, "__findViewWithAttributeInTree: checking if root=%s has attr=%s == %s" % (root.__smallStr__(), attr, val)
    2123
    2124          if isinstance(val, RegexType):
    2125              return self.__findViewWithAttributeInTreeThatMatches(attr, val, root)
    2126          else:
    2127              if root and attr in root.map and root.map[attr] == val:
    2128                  if DEBUG: print >>sys.stderr, "__findViewWithAttributeInTree:  FOUND: %s" % root.__smallStr__()
    2129                  return root
    2130              else:
    2131                  for ch in root.children:
    2132                      v = self.__findViewWithAttributeInTree(attr, val, ch)
    2133                      if v:
    2134                          return v
    2135
    2136          return None
    2137
    2138 -    def __findViewWithAttributeInTreeOrRaise(self, attr, val, root):
    2139          view = self.__findViewWithAttributeInTree(attr, val, root)
    2140          if view:
    2141              return view
    2142          else:
    2143              raise ViewNotFoundException(attr, val, root)
    2144
    2145 -    def __findViewWithAttributeInTreeThatMatches(self, attr, regex, root, rlist=[]):
    2146          if not self.root:
    2147              print >>sys.stderr, "ERROR: no root, did you forget to call dump()?"
    2148              return None
    2149
    2150          if type(root) == types.StringType and root == "ROOT":
    2151              root = self.root
    2152
    2153          if DEBUG: print >>sys.stderr, "__findViewWithAttributeInTreeThatMatches: checking if root=%s attr=%s matches %s" % (root.__smallStr__(), attr, regex)
    2154
    2155          if root and attr in root.map and regex.match(root.map[attr]):
    2156              if DEBUG: print >>sys.stderr, "__findViewWithAttributeInTreeThatMatches:  FOUND: %s" % root.__smallStr__()
    2157              return root
    2158              #print >>sys.stderr, "appending root=%s to rlist=%s" % (root.__smallStr__(), rlist)
    2159              #return rlist.append(root)
    2160          else:
    2161              for ch in root.children:
    2162                  v = self.__findViewWithAttributeInTreeThatMatches(attr, regex, ch, rlist)
    2163                  if v:
    2164                      return v
    2165                      #print >>sys.stderr, "appending v=%s to rlist=%s" % (v.__smallStr__(), rlist)
    2166                      #return rlist.append(v)
    2167
    2168          return None
    2169          #return rlist
    2170
    2171 -    def findViewWithAttribute(self, attr, val, root="ROOT"):
    2172          '''
    2173          Finds the View with the specified attribute and value
    2174          '''
    2175
    2176          return self.__findViewWithAttributeInTree(attr, val, root)
    2177
    2178 -    def findViewWithAttributeOrRaise(self, attr, val, root="ROOT"):
    2179          '''
    2180          Finds the View or raise a ViewNotFoundException.
    2181
    2182          @return: the View found
    2183          @raise ViewNotFoundException: raise the exception if View not found
    2184          '''
    2185
    2186          view = self.findViewWithAttribute(attr, val, root)
    2187          if view:
    2188              return view
    2189          else:
    2190              raise ViewNotFoundException(attr, val, root)
    2191
    2192 -    def findViewWithAttributeThatMatches(self, attr, regex, root="ROOT"):
    2193          '''
    2194          Finds the list of Views with the specified attribute matching
    2195          regex
    2196          '''
    2197
    2198          return self.__findViewWithAttributeInTreeThatMatches(attr, regex, root)
    2199
    2200 -    def findViewWithText(self, text, root="ROOT"):
    2201          if DEBUG:
    2202              print >>sys.stderr, "findViewWithText(%s, %s)" % (text, root)
    2203          if isinstance(text, RegexType):
    2204              return self.findViewWithAttributeThatMatches(self.textProperty, text, root)
    2205              #l = self.findViewWithAttributeThatMatches(TEXT_PROPERTY, text)
    2206              #ll = len(l)
    2207              #if ll == 0:
    2208              #    return None
    2209              #elif ll == 1:
    2210              #    return l[0]
    2211              #else:
    2212              #    print >>sys.stderr, "WARNING: findViewWithAttributeThatMatches invoked by findViewWithText returns %d items." % ll
    2213              #    return l
    2214          else:
    2215              return self.findViewWithAttribute(self.textProperty, text, root)
    2216
    2217 -    def findViewWithTextOrRaise(self, text, root="ROOT"):
    2218          '''
    2219          Finds the View or raise a ViewNotFoundException.
    2220
    2221          @return: the View found
    2222          @raise ViewNotFoundException: raise the exception if View not found
    2223          '''
    2224
    2225          if DEBUG:
    2226              print >>sys.stderr, "findViewWithTextOrRaise(%s, %s)" % (text, root)
    2227          view = self.findViewWithText(text, root)
    2228          if view:
    2229              return view
    2230          else:
    2231              raise ViewNotFoundException("text", text, root)
    2232
    2233 -    def findViewWithContentDescription(self, contentdescription, root="ROOT"):
    2234          '''
    2235          Finds the View with the specified content description
    2236          '''
    2237
    2238          return self.__findViewWithAttributeInTree('content-desc', contentdescription, root)
    2239
    2240 -    def findViewWithContentDescriptionOrRaise(self, contentdescription, root="ROOT"):
    2241          '''
    2242          Finds the View with the specified content description
    2243          '''
    2244
    2245          return self.__findViewWithAttributeInTreeOrRaise('content-desc', contentdescription, root)
    2246
    2247 -    def findViewsContainingPoint(self, (x, y), filter=None):
    2248          '''
    2249          Finds the list of Views that contain the point (x, y).
    2250          '''
    2251
    2252          if not filter:
    2253              filter = lambda v: True
    2254
    2255          return [v for v in self.views if (v.containsPoint((x,y)) and filter(v))]
    2256
    2257 -    def getViewIds(self):
    2258          '''
    2259          @deprecated: Use L{getViewsById} instead.
    2260
    2261          Returns the Views map.
    2262          '''
    2263
    2264          return self.viewsById
    2265
    2266 -    def getViewsById(self):
    2267          '''
    2268          Returns the Views map. The keys are C{uniqueIds} and the values are C{View}s.
    2269          '''
    2270
    2271          return self.viewsById
    2272
    2273 -    def __getFocusedWindowPosition(self):
    2274          return self.__getFocusedWindowId()
    2275
    2276 -    def getSdkVersion(self):
    2277          '''
    2278          Gets the SDK version.
    2279          '''
    2280
    2281          return self.build[VERSION_SDK_PROPERTY]
    2282
    2283 -    def isKeyboardShown(self):
    2284          '''
    2285          Whether the keyboard is displayed.
    2286          '''
    2287
    2288          dim = self.device.shell('dumpsys input_method')
    2289          if dim:
    2290              # FIXME: API >= 15 ?
    2291              return "mInputShown=true" in dim
    2292          return False
    2293
    2294 -    def writeImageToFile(self, filename, format="PNG"):
    2295          '''
    2296          Write the View image to the specified filename in the specified format.
    2297
    2298          @type filename: str
    2299          @param filename: Absolute path and optional filename receiving the image. If this points to
    2300                           a directory, then the filename is determined by the serialno of the device and
    2301                           format extension.
    2302          @type format: str
    2303          @param format: Image format (default format is PNG)
    2304          '''
    2305
    2306          if not os.path.isabs(filename):
    2307              raise ValueError("writeImageToFile expects an absolute path")
    2308          if os.path.isdir(filename):
    2309              filename = os.path.join(filename, self.serialno + '.' + format.lower())
    2310          if DEBUG:
    2311              print >> sys.stderr, "writeImageToFile: saving image to '%s' in %s format" % (filename, format)
    2312          self.device.takeSnapshot().save(filename, format)
    2313
    2314      @staticmethod
    2315 -    def __pickleable(tree):
    2316          '''
    2317          Makes the tree pickleable.
    2318          '''
    2319
    2320          def removeDeviceReference(view):
    2321              '''
    2322              Removes the reference to a L{MonkeyDevice}.
    2323              '''
    2324
    2325              view.device = None
    2326
    2327          ###########################################################################################
    2328          # FIXME: Unfortunatelly deepcopy does not work with MonkeyDevice objects, which is
    2329          # sadly the reason why we cannot pickle the tree and we need to remove the MonkeyDevice
    2330          # references.
    2331          # We wanted to copy the tree to preserve the original and make piclkleable the copy.
    2332          #treeCopy = copy.deepcopy(tree)
    2333          treeCopy = tree
    2334          # IMPORTANT:
    2335          # This assumes that the first element in the list is the tree root
    2336          ViewClient.__traverse(treeCopy[0], transform=removeDeviceReference)
    2337          ###########################################################################################
    2338          return treeCopy
    2339
    2340 -    def distance(self, tree):
    2341          '''
    2342          Calculates the distance between this tree and the tree passed as argument.
    2343
    2344          @type tree: list of Views
    2345          @param tree: Tree of Views
    2346          @return: the distance
    2347          '''
    2348          ################################################################
    2349          #FIXME: this should copy the entire tree and then transform it #
    2350          ################################################################
    2351          pickleableViews = ViewClient.__pickleable(self.views)
    2352          pickleableTree = ViewClient.__pickleable(tree)
    2353          s1 = pickle.dumps(pickleableViews)
    2354          s2 = pickle.dumps(pickleableTree)
    2355
    2356          if DEBUG_DISTANCE:
    2357              print >>sys.stderr, "distance: calculating distance between", s1[:20], "and", s2[:20]
    2358
    2359          l1 = len(s1)
    2360          l2 = len(s2)
    2361          t = float(max(l1, l2))
    2362
    2363          if l1 == l2:
    2364              if DEBUG_DISTANCE:
    2365                  print >>sys.stderr, "distance: trees have same length, using Hamming distance"
    2366              return ViewClient.__hammingDistance(s1, s2)/t
    2367          else:
    2368              if DEBUG_DISTANCE:
    2369                  print >>sys.stderr, "distance: trees have different length, using Levenshtein distance"
    2370              return ViewClient.__levenshteinDistance(s1, s2)/t
    2371
    2372      @staticmethod
    2373 -    def __hammingDistance(s1, s2):
    2374          '''
    2375          Finds the Hamming distance between two strings.
    2376
    2377          @param s1: string
    2378          @param s2: string
    2379          @return: the distance
    2380          @raise ValueError: if the lenght of the strings differ
    2381          '''
    2382
    2383          l1 = len(s1)
    2384          l2 = len(s2)
    2385
    2386          if l1 != l2:
    2387              raise ValueError("Hamming distance requires strings of same size.")
    2388
    2389          return sum(ch1 != ch2 for ch1, ch2 in zip(s1, s2))
    2390
    2391 -    def hammingDistance(self, tree):
    2392          '''
    2393          Finds the Hamming distance between this tree and the one passed as argument.
    2394          '''
    2395
    2396          s1 = ' '.join(map(View.__str__, self.views))
    2397          s2 = ' '.join(map(View.__str__, tree))
    2398
    2399          return ViewClient.__hammingDistance(s1, s2)
    2400
    2401      @staticmethod
    2402 -    def __levenshteinDistance(s, t):
    2403          '''
    2404          Find the Levenshtein distance between two Strings.
    2405
    2406          Python version of Levenshtein distance method implemented in Java at
    2407          U{http://www.java2s.com/Code/Java/Data-Type/FindtheLevenshteindistancebetweentwoStrings.htm}.
    2408
    2409          This is the number of changes needed to change one String into
    2410          another, where each change is a single character modification (deletion,
    2411          insertion or substitution).
    2412
    2413          The previous implementation of the Levenshtein distance algorithm
    2414          was from U{http://www.merriampark.com/ld.htm}
    2415
    2416          Chas Emerick has written an implementation in Java, which avoids an OutOfMemoryError
    2417          which can occur when my Java implementation is used with very large strings.
    2418          This implementation of the Levenshtein distance algorithm
    2419          is from U{http://www.merriampark.com/ldjava.htm}::
    2420
    2421              StringUtils.getLevenshteinDistance(null, *)             = IllegalArgumentException
    2422              StringUtils.getLevenshteinDistance(*, null)             = IllegalArgumentException
    2423              StringUtils.getLevenshteinDistance("","")               = 0
    2424              StringUtils.getLevenshteinDistance("","a")              = 1
    2425              StringUtils.getLevenshteinDistance("aaapppp", "")       = 7
    2426              StringUtils.getLevenshteinDistance("frog", "fog")       = 1
    2427              StringUtils.getLevenshteinDistance("fly", "ant")        = 3
    2428              StringUtils.getLevenshteinDistance("elephant", "hippo") = 7
    2429              StringUtils.getLevenshteinDistance("hippo", "elephant") = 7
    2430              StringUtils.getLevenshteinDistance("hippo", "zzzzzzzz") = 8
    2431              StringUtils.getLevenshteinDistance("hello", "hallo")    = 1
    2432
    2433          @param s:  the first String, must not be null
    2434          @param t:  the second String, must not be null
    2435          @return: result distance
    2436          @raise ValueError: if either String input C{null}
    2437          '''
    2438          if s is None or t is None:
    2439              raise ValueError("Strings must not be null")
    2440
    2441          n = len(s)
    2442          m = len(t)
    2443
    2444          if n == 0:
    2445              return m
    2446          elif m == 0:
    2447              return n
    2448
    2449          if n > m:
    2450              tmp = s
    2451              s = t
    2452              t = tmp
    2453              n = m;
    2454              m = len(t)
    2455
    2456          p = [None]*(n+1)
    2457          d = [None]*(n+1)
    2458
    2459          for i in range(0, n+1):
    2460              p[i] = i
    2461
    2462          for j in range(1, m+1):
    2463              if DEBUG_DISTANCE:
    2464                  if j % 100 == 0:
    2465                      print >>sys.stderr, "DEBUG:", int(j/(m+1.0)*100),"%\r",
    2466              t_j = t[j-1]
    2467              d[0] = j
    2468
    2469              for i in range(1, n+1):
    2470                  cost = 0 if s[i-1] == t_j else 1
    2471                  #  minimum of cell to the left+1, to the top+1, diagonally left and up +cost
    2472                  d[i] = min(min(d[i-1]+1, p[i]+1), p[i-1]+cost)
    2473
    2474              _d = p
    2475              p = d
    2476              d = _d
    2477
    2478          if DEBUG_DISTANCE:
    2479              print >> sys.stderr, "\n"
    2480          return p[n]
    2481
    2482 -    def levenshteinDistance(self, tree):
    2483          '''
    2484          Finds the Levenshtein distance between this tree and the one passed as argument.
    2485          '''
    2486
    2487          s1 = ' '.join(map(View.__microStr__, self.views))
    2488          s2 = ' '.join(map(View.__microStr__, tree))
    2489
    2490          return ViewClient.__levenshteinDistance(s1, s2)
    2491
    2492      @staticmethod
    2493 -    def excerpt(str, execute=False):
    2494          code = Excerpt2Code().Parse(str)
    2495          if execute:
    2496              exec code
    2497          else:
    2498              return code
    2499
    2500
    2501  if __name__ == "__main__":
    2502      try:
    2503          vc = ViewClient(None)
    2504      except:
    2505          print "%s: Don't expect this to do anything" % __file__
    2506