1# Copyright 2013 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import its.error
16import os
17import os.path
18import sys
19import re
20import json
21import time
22import unittest
23import socket
24import subprocess
25import hashlib
26import numpy
27
28class ItsSession(object):
29    """Controls a device over adb to run ITS scripts.
30
31    The script importing this module (on the host machine) prepares JSON
32    objects encoding CaptureRequests, specifying sets of parameters to use
33    when capturing an image using the Camera2 APIs. This class encapsulates
34    sending the requests to the device, monitoring the device's progress, and
35    copying the resultant captures back to the host machine when done. TCP
36    forwarded over adb is the transport mechanism used.
37
38    The device must have CtsVerifier.apk installed.
39
40    Attributes:
41        sock: The open socket.
42    """
43
44    # Open a connection to localhost:<host_port>, forwarded to port 6000 on the
45    # device. <host_port> is determined at run-time to support multiple
46    # connected devices.
47    IPADDR = '127.0.0.1'
48    REMOTE_PORT = 6000
49    BUFFER_SIZE = 4096
50
51    # LOCK_PORT is used as a mutex lock to protect the list of forwarded ports
52    # among all processes. The script assumes LOCK_PORT is available and will
53    # try to use ports between CLIENT_PORT_START and
54    # CLIENT_PORT_START+MAX_NUM_PORTS-1 on host for ITS sessions.
55    CLIENT_PORT_START = 6000
56    MAX_NUM_PORTS = 100
57    LOCK_PORT = CLIENT_PORT_START + MAX_NUM_PORTS
58
59    # Seconds timeout on each socket operation.
60    SOCK_TIMEOUT = 10.0
61    SEC_TO_NSEC = 1000*1000*1000.0
62
63    PACKAGE = 'com.android.cts.verifier.camera.its'
64    INTENT_START = 'com.android.cts.verifier.camera.its.START'
65    ACTION_ITS_RESULT = 'com.android.cts.verifier.camera.its.ACTION_ITS_RESULT'
66    EXTRA_CAMERA_ID = 'camera.its.extra.CAMERA_ID'
67    EXTRA_SUCCESS = 'camera.its.extra.SUCCESS'
68    EXTRA_SUMMARY = 'camera.its.extra.SUMMARY'
69
70    adb = "adb -d"
71    device_id = ""
72
73    # Definitions for some of the common output format options for do_capture().
74    # Each gets images of full resolution for each requested format.
75    CAP_RAW = {"format":"raw"}
76    CAP_DNG = {"format":"dng"}
77    CAP_YUV = {"format":"yuv"}
78    CAP_JPEG = {"format":"jpeg"}
79    CAP_RAW_YUV = [{"format":"raw"}, {"format":"yuv"}]
80    CAP_DNG_YUV = [{"format":"dng"}, {"format":"yuv"}]
81    CAP_RAW_JPEG = [{"format":"raw"}, {"format":"jpeg"}]
82    CAP_DNG_JPEG = [{"format":"dng"}, {"format":"jpeg"}]
83    CAP_YUV_JPEG = [{"format":"yuv"}, {"format":"jpeg"}]
84    CAP_RAW_YUV_JPEG = [{"format":"raw"}, {"format":"yuv"}, {"format":"jpeg"}]
85    CAP_DNG_YUV_JPEG = [{"format":"dng"}, {"format":"yuv"}, {"format":"jpeg"}]
86
87    # Initialize the socket port for the host to forward requests to the device.
88    # This method assumes localhost's LOCK_PORT is available and will try to
89    # use ports between CLIENT_PORT_START and CLIENT_PORT_START+MAX_NUM_PORTS-1
90    def __init_socket_port(self):
91        NUM_RETRIES = 100
92        RETRY_WAIT_TIME_SEC = 0.05
93
94        # Bind a socket to use as mutex lock
95        socket_lock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
96        for i in range(NUM_RETRIES):
97            try:
98                socket_lock.bind((ItsSession.IPADDR, ItsSession.LOCK_PORT))
99                break
100            except socket.error:
101                if i == NUM_RETRIES - 1:
102                    raise its.error.Error(self.device_id,
103                                          "acquiring socket lock timed out")
104                else:
105                    time.sleep(RETRY_WAIT_TIME_SEC)
106
107        # Check if a port is already assigned to the device.
108        command = "adb forward --list"
109        proc = subprocess.Popen(command.split(), stdout=subprocess.PIPE)
110        output, error = proc.communicate()
111
112        port = None
113        used_ports = []
114        for line in output.split(os.linesep):
115            # each line should be formatted as:
116            # "<device_id> tcp:<host_port> tcp:<remote_port>"
117            forward_info = line.split()
118            if len(forward_info) >= 3 and \
119               len(forward_info[1]) > 4 and forward_info[1][:4] == "tcp:" and \
120               len(forward_info[2]) > 4 and forward_info[2][:4] == "tcp:":
121                local_p = int(forward_info[1][4:])
122                remote_p = int(forward_info[2][4:])
123                if forward_info[0] == self.device_id and \
124                   remote_p == ItsSession.REMOTE_PORT:
125                    port = local_p
126                    break;
127                else:
128                    used_ports.append(local_p)
129
130        # Find the first available port if no port is assigned to the device.
131        if port is None:
132            for p in range(ItsSession.CLIENT_PORT_START,
133                           ItsSession.CLIENT_PORT_START +
134                           ItsSession.MAX_NUM_PORTS):
135                if p not in used_ports:
136                    # Try to run "adb forward" with the port
137                    command = "%s forward tcp:%d tcp:%d" % \
138                              (self.adb, p, self.REMOTE_PORT)
139                    proc = subprocess.Popen(command.split(),
140                                            stdout=subprocess.PIPE,
141                                            stderr=subprocess.PIPE)
142                    output, error = proc.communicate()
143
144                    # Check if there is no error
145                    if error is None or error.find("error") < 0:
146                        port = p
147                        break
148
149        if port is None:
150            raise its.error.Error(self.device_id, " cannot find an available " +
151                                  "port")
152
153        # Release the socket as mutex unlock
154        socket_lock.close()
155
156        # Connect to the socket
157        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
158        self.sock.connect((self.IPADDR, port))
159        self.sock.settimeout(self.SOCK_TIMEOUT)
160
161    # Reboot the device if needed and wait for the service to be ready for
162    # connection.
163    def __wait_for_service(self):
164        # This also includes the optional reboot handling: if the user
165        # provides a "reboot" or "reboot=N" arg, then reboot the device,
166        # waiting for N seconds (default 30) before returning.
167        for s in sys.argv[1:]:
168            if s[:6] == "reboot":
169                duration = 30
170                if len(s) > 7 and s[6] == "=":
171                    duration = int(s[7:])
172                print "Rebooting device"
173                _run("%s reboot" % (self.adb));
174                _run("%s wait-for-device" % (self.adb))
175                time.sleep(duration)
176                print "Reboot complete"
177
178        # TODO: Figure out why "--user 0" is needed, and fix the problem.
179        _run('%s shell am force-stop --user 0 %s' % (self.adb, self.PACKAGE))
180        _run(('%s shell am startservice --user 0 -t text/plain '
181              '-a %s') % (self.adb, self.INTENT_START))
182
183        # Wait until the socket is ready to accept a connection.
184        proc = subprocess.Popen(
185                self.adb.split() + ["logcat"],
186                stdout=subprocess.PIPE)
187        logcat = proc.stdout
188        while True:
189            line = logcat.readline().strip()
190            if line.find('ItsService ready') >= 0:
191                break
192        proc.kill()
193
194    def __init__(self):
195        # Initialize device id and adb command.
196        self.device_id = get_device_id()
197        self.adb = "adb -s " + self.device_id
198
199        self.__wait_for_service()
200        self.__init_socket_port()
201
202        self.__close_camera()
203        self.__open_camera()
204
205    def __del__(self):
206        if hasattr(self, 'sock') and self.sock:
207            self.__close_camera()
208            self.sock.close()
209
210    def __enter__(self):
211        return self
212
213    def __exit__(self, type, value, traceback):
214        return False
215
216    def __read_response_from_socket(self):
217        # Read a line (newline-terminated) string serialization of JSON object.
218        chars = []
219        while len(chars) == 0 or chars[-1] != '\n':
220            ch = self.sock.recv(1)
221            if len(ch) == 0:
222                # Socket was probably closed; otherwise don't get empty strings
223                raise its.error.Error('Problem with socket on device side')
224            chars.append(ch)
225        line = ''.join(chars)
226        jobj = json.loads(line)
227        # Optionally read a binary buffer of a fixed size.
228        buf = None
229        if jobj.has_key("bufValueSize"):
230            n = jobj["bufValueSize"]
231            buf = bytearray(n)
232            view = memoryview(buf)
233            while n > 0:
234                nbytes = self.sock.recv_into(view, n)
235                view = view[nbytes:]
236                n -= nbytes
237            buf = numpy.frombuffer(buf, dtype=numpy.uint8)
238        return jobj, buf
239
240    def __open_camera(self):
241        # Get the camera ID to open as an argument.
242        camera_id = 0
243        for s in sys.argv[1:]:
244            if s[:7] == "camera=" and len(s) > 7:
245                camera_id = int(s[7:])
246        cmd = {"cmdName":"open", "cameraId":camera_id}
247        self.sock.send(json.dumps(cmd) + "\n")
248        data,_ = self.__read_response_from_socket()
249        if data['tag'] != 'cameraOpened':
250            raise its.error.Error('Invalid command response')
251
252    def __close_camera(self):
253        cmd = {"cmdName":"close"}
254        self.sock.send(json.dumps(cmd) + "\n")
255        data,_ = self.__read_response_from_socket()
256        if data['tag'] != 'cameraClosed':
257            raise its.error.Error('Invalid command response')
258
259    def do_vibrate(self, pattern):
260        """Cause the device to vibrate to a specific pattern.
261
262        Args:
263            pattern: Durations (ms) for which to turn on or off the vibrator.
264                The first value indicates the number of milliseconds to wait
265                before turning the vibrator on. The next value indicates the
266                number of milliseconds for which to keep the vibrator on
267                before turning it off. Subsequent values alternate between
268                durations in milliseconds to turn the vibrator off or to turn
269                the vibrator on.
270
271        Returns:
272            Nothing.
273        """
274        cmd = {}
275        cmd["cmdName"] = "doVibrate"
276        cmd["pattern"] = pattern
277        self.sock.send(json.dumps(cmd) + "\n")
278        data,_ = self.__read_response_from_socket()
279        if data['tag'] != 'vibrationStarted':
280            raise its.error.Error('Invalid command response')
281
282    def start_sensor_events(self):
283        """Start collecting sensor events on the device.
284
285        See get_sensor_events for more info.
286
287        Returns:
288            Nothing.
289        """
290        cmd = {}
291        cmd["cmdName"] = "startSensorEvents"
292        self.sock.send(json.dumps(cmd) + "\n")
293        data,_ = self.__read_response_from_socket()
294        if data['tag'] != 'sensorEventsStarted':
295            raise its.error.Error('Invalid command response')
296
297    def get_sensor_events(self):
298        """Get a trace of all sensor events on the device.
299
300        The trace starts when the start_sensor_events function is called. If
301        the test runs for a long time after this call, then the device's
302        internal memory can fill up. Calling get_sensor_events gets all events
303        from the device, and then stops the device from collecting events and
304        clears the internal buffer; to start again, the start_sensor_events
305        call must be used again.
306
307        Events from the accelerometer, compass, and gyro are returned; each
308        has a timestamp and x,y,z values.
309
310        Note that sensor events are only produced if the device isn't in its
311        standby mode (i.e.) if the screen is on.
312
313        Returns:
314            A Python dictionary with three keys ("accel", "mag", "gyro") each
315            of which maps to a list of objects containing "time","x","y","z"
316            keys.
317        """
318        cmd = {}
319        cmd["cmdName"] = "getSensorEvents"
320        self.sock.send(json.dumps(cmd) + "\n")
321        data,_ = self.__read_response_from_socket()
322        if data['tag'] != 'sensorEvents':
323            raise its.error.Error('Invalid command response')
324        return data['objValue']
325
326    def get_camera_ids(self):
327        """Get a list of camera device Ids that can be opened.
328
329        Returns:
330            a list of camera ID string
331        """
332        cmd = {}
333        cmd["cmdName"] = "getCameraIds"
334        self.sock.send(json.dumps(cmd) + "\n")
335        data,_ = self.__read_response_from_socket()
336        if data['tag'] != 'cameraIds':
337            raise its.error.Error('Invalid command response')
338        return data['objValue']['cameraIdArray']
339
340    def get_camera_properties(self):
341        """Get the camera properties object for the device.
342
343        Returns:
344            The Python dictionary object for the CameraProperties object.
345        """
346        cmd = {}
347        cmd["cmdName"] = "getCameraProperties"
348        self.sock.send(json.dumps(cmd) + "\n")
349        data,_ = self.__read_response_from_socket()
350        if data['tag'] != 'cameraProperties':
351            raise its.error.Error('Invalid command response')
352        return data['objValue']['cameraProperties']
353
354    def do_3a(self, regions_ae=[[0,0,1,1,1]],
355                    regions_awb=[[0,0,1,1,1]],
356                    regions_af=[[0,0,1,1,1]],
357                    do_ae=True, do_awb=True, do_af=True,
358                    lock_ae=False, lock_awb=False,
359                    get_results=False,
360                    ev_comp=0):
361        """Perform a 3A operation on the device.
362
363        Triggers some or all of AE, AWB, and AF, and returns once they have
364        converged. Uses the vendor 3A that is implemented inside the HAL.
365
366        Throws an assertion if 3A fails to converge.
367
368        Args:
369            regions_ae: List of weighted AE regions.
370            regions_awb: List of weighted AWB regions.
371            regions_af: List of weighted AF regions.
372            do_ae: Trigger AE and wait for it to converge.
373            do_awb: Wait for AWB to converge.
374            do_af: Trigger AF and wait for it to converge.
375            lock_ae: Request AE lock after convergence, and wait for it.
376            lock_awb: Request AWB lock after convergence, and wait for it.
377            get_results: Return the 3A results from this function.
378            ev_comp: An EV compensation value to use when running AE.
379
380        Region format in args:
381            Arguments are lists of weighted regions; each weighted region is a
382            list of 5 values, [x,y,w,h, wgt], and each argument is a list of
383            these 5-value lists. The coordinates are given as normalized
384            rectangles (x,y,w,h) specifying the region. For example:
385                [[0.0, 0.0, 1.0, 0.5, 5], [0.0, 0.5, 1.0, 0.5, 10]].
386            Weights are non-negative integers.
387
388        Returns:
389            Five values are returned if get_results is true::
390            * AE sensitivity; None if do_ae is False
391            * AE exposure time; None if do_ae is False
392            * AWB gains (list); None if do_awb is False
393            * AWB transform (list); None if do_awb is false
394            * AF focus position; None if do_af is false
395            Otherwise, it returns five None values.
396        """
397        print "Running vendor 3A on device"
398        cmd = {}
399        cmd["cmdName"] = "do3A"
400        cmd["regions"] = {"ae": sum(regions_ae, []),
401                          "awb": sum(regions_awb, []),
402                          "af": sum(regions_af, [])}
403        cmd["triggers"] = {"ae": do_ae, "af": do_af}
404        if lock_ae:
405            cmd["aeLock"] = True
406        if lock_awb:
407            cmd["awbLock"] = True
408        if ev_comp != 0:
409            cmd["evComp"] = ev_comp
410        self.sock.send(json.dumps(cmd) + "\n")
411
412        # Wait for each specified 3A to converge.
413        ae_sens = None
414        ae_exp = None
415        awb_gains = None
416        awb_transform = None
417        af_dist = None
418        converged = False
419        while True:
420            data,_ = self.__read_response_from_socket()
421            vals = data['strValue'].split()
422            if data['tag'] == 'aeResult':
423                ae_sens, ae_exp = [int(i) for i in vals]
424            elif data['tag'] == 'afResult':
425                af_dist = float(vals[0])
426            elif data['tag'] == 'awbResult':
427                awb_gains = [float(f) for f in vals[:4]]
428                awb_transform = [float(f) for f in vals[4:]]
429            elif data['tag'] == '3aConverged':
430                converged = True
431            elif data['tag'] == '3aDone':
432                break
433            else:
434                raise its.error.Error('Invalid command response')
435        if converged and not get_results:
436            return None,None,None,None,None
437        if (do_ae and ae_sens == None or do_awb and awb_gains == None
438                or do_af and af_dist == None or not converged):
439            raise its.error.Error('3A failed to converge')
440        return ae_sens, ae_exp, awb_gains, awb_transform, af_dist
441
442    def do_capture(self, cap_request, out_surfaces=None, reprocess_format=None):
443        """Issue capture request(s), and read back the image(s) and metadata.
444
445        The main top-level function for capturing one or more images using the
446        device. Captures a single image if cap_request is a single object, and
447        captures a burst if it is a list of objects.
448
449        The out_surfaces field can specify the width(s), height(s), and
450        format(s) of the captured image. The formats may be "yuv", "jpeg",
451        "dng", "raw", "raw10", or "raw12". The default is a YUV420 frame ("yuv")
452        corresponding to a full sensor frame.
453
454        Note that one or more surfaces can be specified, allowing a capture to
455        request images back in multiple formats (e.g.) raw+yuv, raw+jpeg,
456        yuv+jpeg, raw+yuv+jpeg. If the size is omitted for a surface, the
457        default is the largest resolution available for the format of that
458        surface. At most one output surface can be specified for a given format,
459        and raw+dng, raw10+dng, and raw+raw10 are not supported as combinations.
460
461        If reprocess_format is not None, for each request, an intermediate
462        buffer of the given reprocess_format will be captured from camera and
463        the intermediate buffer will be reprocessed to the output surfaces. The
464        following settings will be turned off when capturing the intermediate
465        buffer and will be applied when reprocessing the intermediate buffer.
466            1. android.noiseReduction.mode
467            2. android.edge.mode
468            3. android.reprocess.effectiveExposureFactor
469
470        Supported reprocess format are "yuv" and "private". Supported output
471        surface formats when reprocessing is enabled are "yuv" and "jpeg".
472
473        Example of a single capture request:
474
475            {
476                "android.sensor.exposureTime": 100*1000*1000,
477                "android.sensor.sensitivity": 100
478            }
479
480        Example of a list of capture requests:
481
482            [
483                {
484                    "android.sensor.exposureTime": 100*1000*1000,
485                    "android.sensor.sensitivity": 100
486                },
487                {
488                    "android.sensor.exposureTime": 100*1000*1000,
489                    "android.sensor.sensitivity": 200
490                }
491            ]
492
493        Examples of output surface specifications:
494
495            {
496                "width": 640,
497                "height": 480,
498                "format": "yuv"
499            }
500
501            [
502                {
503                    "format": "jpeg"
504                },
505                {
506                    "format": "raw"
507                }
508            ]
509
510        The following variables defined in this class are shortcuts for
511        specifying one or more formats where each output is the full size for
512        that format; they can be used as values for the out_surfaces arguments:
513
514            CAP_RAW
515            CAP_DNG
516            CAP_YUV
517            CAP_JPEG
518            CAP_RAW_YUV
519            CAP_DNG_YUV
520            CAP_RAW_JPEG
521            CAP_DNG_JPEG
522            CAP_YUV_JPEG
523            CAP_RAW_YUV_JPEG
524            CAP_DNG_YUV_JPEG
525
526        If multiple formats are specified, then this function returns multiple
527        capture objects, one for each requested format. If multiple formats and
528        multiple captures (i.e. a burst) are specified, then this function
529        returns multiple lists of capture objects. In both cases, the order of
530        the returned objects matches the order of the requested formats in the
531        out_surfaces parameter. For example:
532
533            yuv_cap            = do_capture( req1                           )
534            yuv_cap            = do_capture( req1,        yuv_fmt           )
535            yuv_cap,  raw_cap  = do_capture( req1,        [yuv_fmt,raw_fmt] )
536            yuv_caps           = do_capture( [req1,req2], yuv_fmt           )
537            yuv_caps, raw_caps = do_capture( [req1,req2], [yuv_fmt,raw_fmt] )
538
539        Args:
540            cap_request: The Python dict/list specifying the capture(s), which
541                will be converted to JSON and sent to the device.
542            out_surfaces: (Optional) specifications of the output image formats
543                and sizes to use for each capture.
544            reprocess_format: (Optional) The reprocessing format. If not None,
545                reprocessing will be enabled.
546
547        Returns:
548            An object, list of objects, or list of lists of objects, where each
549            object contains the following fields:
550            * data: the image data as a numpy array of bytes.
551            * width: the width of the captured image.
552            * height: the height of the captured image.
553            * format: image the format, in ["yuv","jpeg","raw","raw10","dng"].
554            * metadata: the capture result object (Python dictionary).
555        """
556        cmd = {}
557        if reprocess_format != None:
558            cmd["cmdName"] = "doReprocessCapture"
559            cmd["reprocessFormat"] = reprocess_format
560        else:
561            cmd["cmdName"] = "doCapture"
562        if not isinstance(cap_request, list):
563            cmd["captureRequests"] = [cap_request]
564        else:
565            cmd["captureRequests"] = cap_request
566        if out_surfaces is not None:
567            if not isinstance(out_surfaces, list):
568                cmd["outputSurfaces"] = [out_surfaces]
569            else:
570                cmd["outputSurfaces"] = out_surfaces
571            formats = [c["format"] if c.has_key("format") else "yuv"
572                       for c in cmd["outputSurfaces"]]
573            formats = [s if s != "jpg" else "jpeg" for s in formats]
574        else:
575            formats = ['yuv']
576        ncap = len(cmd["captureRequests"])
577        nsurf = 1 if out_surfaces is None else len(cmd["outputSurfaces"])
578        if len(formats) > len(set(formats)):
579            raise its.error.Error('Duplicate format requested')
580        if "dng" in formats and "raw" in formats or \
581                "dng" in formats and "raw10" in formats or \
582                "raw" in formats and "raw10" in formats:
583            raise its.error.Error('Different raw formats not supported')
584
585        # Detect long exposure time and set timeout accordingly
586        longest_exp_time = 0
587        for req in cmd["captureRequests"]:
588            if "android.sensor.exposureTime" in req and \
589                    req["android.sensor.exposureTime"] > longest_exp_time:
590                longest_exp_time = req["android.sensor.exposureTime"]
591
592        extended_timeout = longest_exp_time / self.SEC_TO_NSEC + \
593                self.SOCK_TIMEOUT
594        self.sock.settimeout(extended_timeout)
595
596        print "Capturing %d frame%s with %d format%s [%s]" % (
597                  ncap, "s" if ncap>1 else "", nsurf, "s" if nsurf>1 else "",
598                  ",".join(formats))
599        self.sock.send(json.dumps(cmd) + "\n")
600
601        # Wait for ncap*nsurf images and ncap metadata responses.
602        # Assume that captures come out in the same order as requested in
603        # the burst, however individual images of different formats can come
604        # out in any order for that capture.
605        nbufs = 0
606        bufs = {"yuv":[], "raw":[], "raw10":[], "dng":[], "jpeg":[]}
607        mds = []
608        widths = None
609        heights = None
610        while nbufs < ncap*nsurf or len(mds) < ncap:
611            jsonObj,buf = self.__read_response_from_socket()
612            if jsonObj['tag'] in ['jpegImage', 'yuvImage', 'rawImage', \
613                    'raw10Image', 'dngImage'] and buf is not None:
614                fmt = jsonObj['tag'][:-5]
615                bufs[fmt].append(buf)
616                nbufs += 1
617            elif jsonObj['tag'] == 'captureResults':
618                mds.append(jsonObj['objValue']['captureResult'])
619                outputs = jsonObj['objValue']['outputs']
620                widths = [out['width'] for out in outputs]
621                heights = [out['height'] for out in outputs]
622            else:
623                # Just ignore other tags
624                None
625        rets = []
626        for j,fmt in enumerate(formats):
627            objs = []
628            for i in range(ncap):
629                obj = {}
630                obj["data"] = bufs[fmt][i]
631                obj["width"] = widths[j]
632                obj["height"] = heights[j]
633                obj["format"] = fmt
634                obj["metadata"] = mds[i]
635                objs.append(obj)
636            rets.append(objs if ncap>1 else objs[0])
637        self.sock.settimeout(self.SOCK_TIMEOUT)
638        return rets if len(rets)>1 else rets[0]
639
640def get_device_id():
641    """ Return the ID of the device that the test is running on.
642
643    Return the device ID provided in the command line if it's connected. If no
644    device ID is provided in the command line and there is only one device
645    connected, return the device ID by parsing the result of "adb devices".
646
647    Raise an exception if no device is connected; or the device ID provided in
648    the command line is not connected; or no device ID is provided in the
649    command line and there are more than 1 device connected.
650
651    Returns:
652        Device ID string.
653    """
654    device_id = None
655    for s in sys.argv[1:]:
656        if s[:7] == "device=" and len(s) > 7:
657            device_id = str(s[7:])
658
659    # Get a list of connected devices
660    devices = []
661    command = "adb devices"
662    proc = subprocess.Popen(command.split(), stdout=subprocess.PIPE)
663    output, error = proc.communicate()
664    for line in output.split(os.linesep):
665        device_info = line.split()
666        if len(device_info) == 2 and device_info[1] == "device":
667            devices.append(device_info[0])
668
669    if len(devices) == 0:
670        raise its.error.Error("No device is connected!")
671    elif device_id is not None and device_id not in devices:
672        raise its.error.Error(device_id + " is not connected!")
673    elif device_id is None and len(devices) >= 2:
674        raise its.error.Error("More than 1 device are connected. " +
675                "Use device=<device_id> to specify a device to test.")
676    elif len(devices) == 1:
677        device_id = devices[0]
678
679    return device_id
680
681def report_result(device_id, camera_id, success, summary_path=None):
682    """Send a pass/fail result to the device, via an intent.
683
684    Args:
685        device_id: The ID string of the device to report the results to.
686        camera_id: The ID string of the camera for which to report pass/fail.
687        success: Boolean, indicating if the result was pass or fail.
688        summary_path: (Optional) path to ITS summary file on host PC
689
690    Returns:
691        Nothing.
692    """
693    adb = "adb -s " + device_id
694    device_summary_path = "/sdcard/camera_" + camera_id + "_its_summary.txt"
695    if summary_path is not None:
696        _run("%s push %s %s" % (
697                adb, summary_path, device_summary_path))
698        _run("%s shell am broadcast -a %s --es %s %s --es %s %s --es %s %s" % (
699                adb, ItsSession.ACTION_ITS_RESULT,
700                ItsSession.EXTRA_CAMERA_ID, camera_id,
701                ItsSession.EXTRA_SUCCESS, 'True' if success else 'False',
702                ItsSession.EXTRA_SUMMARY, device_summary_path))
703    else:
704        _run("%s shell am broadcast -a %s --es %s %s --es %s %s --es %s %s" % (
705                adb, ItsSession.ACTION_ITS_RESULT,
706                ItsSession.EXTRA_CAMERA_ID, camera_id,
707                ItsSession.EXTRA_SUCCESS, 'True' if success else 'False',
708                ItsSession.EXTRA_SUMMARY, "null"))
709
710def _run(cmd):
711    """Replacement for os.system, with hiding of stdout+stderr messages.
712    """
713    with open(os.devnull, 'wb') as devnull:
714        subprocess.check_call(
715                cmd.split(), stdout=devnull, stderr=subprocess.STDOUT)
716
717class __UnitTest(unittest.TestCase):
718    """Run a suite of unit tests on this module.
719    """
720
721    # TODO: Add some unit tests.
722    None
723
724if __name__ == '__main__':
725    unittest.main()
726
727