text: # -*- coding: utf-8 -*-
  # minin classes. 
  # use extra resource to enhance original adb device.
  # interface class should have raw_cmd method with a signature 
  # as follows:
  #
  #   def raw_cmd(self, *args, **kwargs):
  #       ...
  #       return subprocess.Popen(...)
  #
  # where `args` is adb command arguments and kwargs are 
  # subprocess keyword arguments.
  
  import os
  import Queue
  import re
  import socket
  import struct
  import subprocess
  import threading
  import time
  import traceback
  
  __dir__ = os.path.dirname(os.path.abspath(__file__))
  
  class RotationWatcherMixin(object):
  
      __rotation = 0
      __watcher_process = None
  
      def open_rotation_watcher(self, on_rotation_change=None):
          package_name = 'jp.co.cyberagent.stf.rotationwatcher'
          out = self.raw_cmd('shell', 'pm', 'list', 'packages', stdout=subprocess.PIPE).communicate()[0]
          if package_name not in out:
              apkpath = os.path.join(__dir__, '..', 'vendor', 'RotationWatcher.apk')
              print 'install rotationwatcher...', apkpath
              if 0 != self.raw_cmd('install', '-rt', apkpath).wait():
                  print 'install rotationwatcher failed.'
                  return
  
          if self.__watcher_process is not None:
              self.__watcher_process.kill()
  
          out = self.raw_cmd('shell', 'pm', 'path', package_name, stdout=subprocess.PIPE).communicate()[0]
          path = out.strip().split(':')[-1]
          p = self.raw_cmd('shell', 
              'CLASSPATH="%s"' % path, 
              'app_process',
              '/system/bin',
              'jp.co.cyberagent.stf.rotationwatcher.RotationWatcher', 
              stdout=subprocess.PIPE)
          self.__watcher_process = p
  
          queue = Queue.Queue()
  
          def _pull():
              while True:
                  line = p.stdout.readline().strip()
                  if not line:
                      if p.poll() is not None:
                          print 'rotationwatcher stopped'
                          break
                      continue
                  queue.put(line)
  
          t = threading.Thread(target=_pull)
          t.setDaemon(True)
          t.start()
  
          def listener(value):
              try:
                  self.__rotation = int(value)/90
              except:
                  return
              if callable(on_rotation_change):
                  on_rotation_change(self.__rotation)
  
          def _listen():
              while True:
                  try:
                      time.sleep(0.005)
                      line = queue.get_nowait()
                      listener(line)
                  except Queue.Empty:
                      if p.poll() is not None:
                          break
                      continue
                  except:
                      traceback.print_exc()
  
          t = threading.Thread(target=_listen)
          t.setDaemon(True)
          t.start()
  
  def str2img(jpgstr, orientation=None):
      import numpy as np
      import cv2
      arr = np.fromstring(jpgstr, np.uint8)
      img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
      if orientation == 1:
          return cv2.flip(cv2.transpose(img), 0) # counter-clockwise
      if orientation == 3:
          return cv2.flip(cv2.transpose(img), 1) # clockwise
      return img
  
  class MinicapStreamMixin(object):
      __screen = None
      __minicap_process = None
  
      def __install_minicap(self):
          # install minicap & minitouch
          os.system('python -m atx minicap')
  
      def open_minicap_stream(self, port=1313):
          # ensure minicap installed
          out = self.raw_cmd('shell', 'ls', '"/data/local/tmp/minicap"', stdout=subprocess.PIPE).communicate()[0]
          if 'No such file or directory' in out:
              self.__install_minicap()
  
          if self.__minicap_process is not None:
              self.__minicap_process.kill()
  
          # if minicap is already started, kill it first.
          out = self.raw_cmd('shell', 'ps', '-C', '/data/local/tmp/minicap', stdout=subprocess.PIPE).communicate()[0]
          out = out.strip().split('\n')
          if len(out) > 1:
              idx = out[0].split().index('PID')
              pid = out[1].split()[idx]
              print 'minicap is running, killing', pid
              self.raw_cmd('shell', 'kill', '-9', pid).wait()
  
          # start minicap
          out = self.raw_cmd('shell', 'LD_LIBRARY_PATH=/data/local/tmp', '/data/local/tmp/minicap', '-i', 
                      stdout=subprocess.PIPE).communicate()[0]
          m = re.search('"width": (\d+).*"height": (\d+).*"rotation": (\d+)', out, re.S)
          w, h, r = map(int, m.groups())
          w, h = min(w, h), max(w, h)
          params = '{x}x{y}@{x}x{y}/{r}'.format(x=w, y=h, r=r)
          print 'starting minicap', params
  
          p = self.raw_cmd('shell', 
                      'LD_LIBRARY_PATH=/data/local/tmp', 
                      '/data/local/tmp/minicap', 
                      '-P %s' % params,
                      '-S',
                      stdout=subprocess.PIPE)
          self.__minicap_process = p
          time.sleep(0.5)
          # forward to tcp port 
          self.raw_cmd('forward', 'tcp:%s' % port, 'localabstract:minicap').wait()
  
          queue = Queue.Queue()
  
          # pull data from socket
          def _pull():
              # print 'start pull', p.pid, p.poll()
              s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
              try:
                  assert p.poll() is None
                  s.connect(('127.0.0.1', port))
                  t = s.recv(24)
                  print 'minicap connected', struct.unpack('<2B5I2B', t)
                  while True:
                      frame_size = struct.unpack("<I", s.recv(4))[0]
                      trunks = []
                      recvd_size = 0
                      while recvd_size < frame_size:
                          trunk_size = min(8192, frame_size-recvd_size)
                          d = s.recv(trunk_size)
                          trunks.append(d)
                          recvd_size += len(d)
                      queue.put(''.join(trunks))
              except Exception as e:
                  if not isinstance(e, struct.error):
                      traceback.print_exc()
                  if p.poll() is not None:
                      print 'Process died.'
                      print p.stdout.read()
                  else:
                      print 'stoping minicap ...'
                      p.kill()
              finally:
                  s.close()
                  self.raw_cmd('forward', '--remove', 'tcp:%s' % port).wait()
  
          t = threading.Thread(target=_pull)
          t.setDaemon(True)
          t.start()
  
          out = self.raw_cmd('shell', 'getprop', 'ro.build.version.sdk', stdout=subprocess.PIPE).communicate()[0]
          sdk = int(out.strip())
          orientation = r/90
  
          def _listen():
              while True:
                  try:
                      time.sleep(0.005)
                      frame = queue.get_nowait()
                      if sdk <= 16:
                          img = str2img(frame, orientation)
                      else:
                          img = str2img(frame)
                      self.__screen = img
                  except Queue.Empty:
                      if p.poll() is not None:
                          print 'minicap died'
                          print p.stdout.read()
                          break
                      continue
                  except:
                      traceback.print_exc()
  
          t = threading.Thread(target=_listen)
          t.setDaemon(True)
          t.start()
  
      def screenshot_cv2(self):
          return self.__screen
  
  class MinitouchStreamMixin(object):
      __touch_queue = None
      __minitouch_process = None
  
      def __install_minitouch(self):
          # install minicap & minitouch
          os.system('python -m atx minicap')
  
      def open_minitouch_stream(self, port=1111):
          if self.__touch_queue is None:
              self.__touch_queue = Queue.Queue()
  
          # ensure minicap installed
          out = self.raw_cmd('shell', 'ls', '"/data/local/tmp/minitouch"', stdout=subprocess.PIPE).communicate()[0]
          if 'No such file or directory' in out:
              self.__install_minitouch()
  
          if self.__minitouch_process is not None:
              self.__minitouch_process.kill()
  
          out = self.raw_cmd('shell', 'ps', '-C', '/data/local/tmp/minitouch', stdout=subprocess.PIPE).communicate()[0]
          out = out.strip().split('\n')
          if len(out) > 1:
              p = None
          else:
              p = self.raw_cmd('shell', '/data/local/tmp/minitouch')
              time.sleep(1)
              if p.poll() is not None:
                  print 'start minitouch failed.'
                  return
          self.__minitouch_process = p                
          self.raw_cmd('forward', 'tcp:%s' % port, 'localabstract:minitouch').wait()
  
          def send():
              s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
              s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
              try:
                  s.connect(('127.0.0.1', port))
                  while True:
                      cmd = self.__touch_queue.get() # wait here
                      if not cmd:
                          continue
                      elif cmd[-1] != '\n':
                          cmd += '\n'
                      s.send(cmd)    
              except:
                  traceback.print_exc()
              finally:
                  s.close()
                  self.raw_cmd('forward', '--remove', 'tcp:%s' % port).wait()
  
          t = threading.Thread(target=send)
          t.setDaemon(True)
          t.start()
  
      def click(self, x, y):
          cmd = 'd 0 %d %d 30\nc\nu 0\nc\n' % (int(x), int(y))
          self.__touch_queue.put(cmd)
  
      def swipe(self, sx, sy, ex, ey, steps=20):
          x1, y1, x2, y2 = map(int, (x1, y1, x2, y2))
          dx = (x2-x1)/steps
          dy = (y2-y1)/steps
          send = self.touchqueue.put
          send('d 0 %d %d 30\nc\n' % (x1, y1))
          for i in range(steps-1):
              x, y = x1+(i+1)*dx, y1+(i+1)*dy
              send('m 0 %d %d 30\nc\n' % (x, y))
          send('u 0 %d %d 30\nc\nu 0\nc\n' % (x2, y2))
  
      def pinchin(self, x1, y1, x2, y2, steps=10):
          pass
  
      def pinchout(self, x1, y1, x2, y2, steps=10):
          pass
  
  class OpenSTFServiceMixin(object):
      pass
  
  
  #-------------- examples ----------------#
  
  class DummyDevice(object):
      def raw_cmd(self, *args, **kwargs):
          cmds = ['adb'] + list(args)
          print cmds
          return subprocess.Popen(cmds, **kwargs)
  
  # Mixins should come in front to override functions in Base
  class TestDevice(MinitouchStreamMixin, MinicapStreamMixin, RotationWatcherMixin, DummyDevice):
      def __init__(self, *args, **kwargs):
          super(self.__class__, self).__init__(*args, **kwargs)
          self.open_rotation_watcher(on_rotation_change=lambda v: self.open_minicap_stream())
          self.open_minitouch_stream()
  
  if __name__ == '__main__':
      import cv2
      dev = TestDevice()
      while True:
          img = dev.screenshot_cv2()
          if img is not None:
              cv2.imshow('screen', img)
          cv2.waitKey(10)
  
  