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 json
16import os
17import socket
18import string
19import subprocess
20import sys
21import time
22import unicodedata
23import unittest
24
25import its.error
26import numpy
27
28from collections import namedtuple
29
30class ItsSession(object):
31    """Controls a device over adb to run ITS scripts.
32
33    The script importing this module (on the host machine) prepares JSON
34    objects encoding CaptureRequests, specifying sets of parameters to use
35    when capturing an image using the Camera2 APIs. This class encapsulates
36    sending the requests to the device, monitoring the device's progress, and
37    copying the resultant captures back to the host machine when done. TCP
38    forwarded over adb is the transport mechanism used.
39
40    The device must have CtsVerifier.apk installed.
41
42    Attributes:
43        sock: The open socket.
44    """
45
46    # Open a connection to localhost:<host_port>, forwarded to port 6000 on the
47    # device. <host_port> is determined at run-time to support multiple
48    # connected devices.
49    IPADDR = '127.0.0.1'
50    REMOTE_PORT = 6000
51    BUFFER_SIZE = 4096
52
53    # LOCK_PORT is used as a mutex lock to protect the list of forwarded ports
54    # among all processes. The script assumes LOCK_PORT is available and will
55    # try to use ports between CLIENT_PORT_START and
56    # CLIENT_PORT_START+MAX_NUM_PORTS-1 on host for ITS sessions.
57    CLIENT_PORT_START = 6000
58    MAX_NUM_PORTS = 100
59    LOCK_PORT = CLIENT_PORT_START + MAX_NUM_PORTS
60
61    # Seconds timeout on each socket operation.
62    SOCK_TIMEOUT = 20.0
63    # Additional timeout in seconds when ITS service is doing more complicated
64    # operations, for example: issuing warmup requests before actual capture.
65    EXTRA_SOCK_TIMEOUT = 5.0
66
67    SEC_TO_NSEC = 1000*1000*1000.0
68
69    PACKAGE = 'com.android.cts.verifier.camera.its'
70    INTENT_START = 'com.android.cts.verifier.camera.its.START'
71    ACTION_ITS_RESULT = 'com.android.cts.verifier.camera.its.ACTION_ITS_RESULT'
72    EXTRA_VERSION = 'camera.its.extra.VERSION'
73    CURRENT_ITS_VERSION = '1.0'  # version number to sync with CtsVerifier
74    EXTRA_CAMERA_ID = 'camera.its.extra.CAMERA_ID'
75    EXTRA_RESULTS = 'camera.its.extra.RESULTS'
76    ITS_TEST_ACTIVITY = 'com.android.cts.verifier/.camera.its.ItsTestActivity'
77
78    # This string must be in sync with ItsService. Updated when interface
79    # between script and ItsService is changed.
80    ITS_SERVICE_VERSION = "1.0"
81
82    RESULT_PASS = 'PASS'
83    RESULT_FAIL = 'FAIL'
84    RESULT_NOT_EXECUTED = 'NOT_EXECUTED'
85    RESULT_VALUES = {RESULT_PASS, RESULT_FAIL, RESULT_NOT_EXECUTED}
86    RESULT_KEY = 'result'
87    SUMMARY_KEY = 'summary'
88    START_TIME_KEY = 'start'
89    END_TIME_KEY = 'end'
90
91    adb = "adb -d"
92    device_id = ""
93
94    # Definitions for some of the common output format options for do_capture().
95    # Each gets images of full resolution for each requested format.
96    CAP_RAW = {"format":"raw"}
97    CAP_DNG = {"format":"dng"}
98    CAP_YUV = {"format":"yuv"}
99    CAP_JPEG = {"format":"jpeg"}
100    CAP_RAW_YUV = [{"format":"raw"}, {"format":"yuv"}]
101    CAP_DNG_YUV = [{"format":"dng"}, {"format":"yuv"}]
102    CAP_RAW_JPEG = [{"format":"raw"}, {"format":"jpeg"}]
103    CAP_DNG_JPEG = [{"format":"dng"}, {"format":"jpeg"}]
104    CAP_YUV_JPEG = [{"format":"yuv"}, {"format":"jpeg"}]
105    CAP_RAW_YUV_JPEG = [{"format":"raw"}, {"format":"yuv"}, {"format":"jpeg"}]
106    CAP_DNG_YUV_JPEG = [{"format":"dng"}, {"format":"yuv"}, {"format":"jpeg"}]
107
108    # Predefine camera props. Save props extracted from the function,
109    # "get_camera_properties".
110    props = None
111
112    # Initialize the socket port for the host to forward requests to the device.
113    # This method assumes localhost's LOCK_PORT is available and will try to
114    # use ports between CLIENT_PORT_START and CLIENT_PORT_START+MAX_NUM_PORTS-1
115    def __init_socket_port(self):
116        NUM_RETRIES = 100
117        RETRY_WAIT_TIME_SEC = 0.05
118
119        # Bind a socket to use as mutex lock
120        socket_lock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
121        for i in range(NUM_RETRIES):
122            try:
123                socket_lock.bind((ItsSession.IPADDR, ItsSession.LOCK_PORT))
124                break
125            except socket.error or socket.timeout:
126                if i == NUM_RETRIES - 1:
127                    raise its.error.Error(self.device_id,
128                                          "socket lock returns error")
129                else:
130                    time.sleep(RETRY_WAIT_TIME_SEC)
131
132        # Check if a port is already assigned to the device.
133        command = "adb forward --list"
134        proc = subprocess.Popen(command.split(), stdout=subprocess.PIPE)
135        output, error = proc.communicate()
136
137        port = None
138        used_ports = []
139        for line in output.split(os.linesep):
140            # each line should be formatted as:
141            # "<device_id> tcp:<host_port> tcp:<remote_port>"
142            forward_info = line.split()
143            if len(forward_info) >= 3 and \
144               len(forward_info[1]) > 4 and forward_info[1][:4] == "tcp:" and \
145               len(forward_info[2]) > 4 and forward_info[2][:4] == "tcp:":
146                local_p = int(forward_info[1][4:])
147                remote_p = int(forward_info[2][4:])
148                if forward_info[0] == self.device_id and \
149                   remote_p == ItsSession.REMOTE_PORT:
150                    port = local_p
151                    break
152                else:
153                    used_ports.append(local_p)
154
155        # Find the first available port if no port is assigned to the device.
156        if port is None:
157            for p in range(ItsSession.CLIENT_PORT_START,
158                           ItsSession.CLIENT_PORT_START +
159                           ItsSession.MAX_NUM_PORTS):
160                if p not in used_ports:
161                    # Try to run "adb forward" with the port
162                    command = "%s forward tcp:%d tcp:%d" % \
163                              (self.adb, p, self.REMOTE_PORT)
164                    proc = subprocess.Popen(command.split(),
165                                            stdout=subprocess.PIPE,
166                                            stderr=subprocess.PIPE)
167                    output, error = proc.communicate()
168
169                    # Check if there is no error
170                    if error is None or error.find("error") < 0:
171                        port = p
172                        break
173
174        if port is None:
175            raise its.error.Error(self.device_id, " cannot find an available " +
176                                  "port")
177
178        # Release the socket as mutex unlock
179        socket_lock.close()
180
181        # Connect to the socket
182        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
183        self.sock.connect((self.IPADDR, port))
184        self.sock.settimeout(self.SOCK_TIMEOUT)
185
186    # Reboot the device if needed and wait for the service to be ready for
187    # connection.
188    def __wait_for_service(self):
189        # This also includes the optional reboot handling: if the user
190        # provides a "reboot" or "reboot=N" arg, then reboot the device,
191        # waiting for N seconds (default 30) before returning.
192        for s in sys.argv[1:]:
193            if s[:6] == "reboot":
194                duration = 30
195                if len(s) > 7 and s[6] == "=":
196                    duration = int(s[7:])
197                print "Rebooting device"
198                _run("%s reboot" % (self.adb))
199                _run("%s wait-for-device" % (self.adb))
200                time.sleep(duration)
201                print "Reboot complete"
202
203        # Flush logcat so following code won't be misled by previous
204        # 'ItsService ready' log.
205        _run('%s logcat -c' % (self.adb))
206        time.sleep(1)
207
208        # TODO: Figure out why "--user 0" is needed, and fix the problem.
209        _run('%s shell am force-stop --user 0 %s' % (self.adb, self.PACKAGE))
210        _run(('%s shell am start-foreground-service --user 0 -t text/plain '
211              '-a %s') % (self.adb, self.INTENT_START))
212
213        # Wait until the socket is ready to accept a connection.
214        proc = subprocess.Popen(
215                self.adb.split() + ["logcat"],
216                stdout=subprocess.PIPE)
217        logcat = proc.stdout
218        while True:
219            line = logcat.readline().strip()
220            if line.find('ItsService ready') >= 0:
221                break
222        proc.kill()
223
224    def __init__(self, camera_id=None, hidden_physical_id=None):
225        self._camera_id = camera_id
226        self._hidden_physical_id = hidden_physical_id
227
228    def __enter__(self):
229        # Initialize device id and adb command.
230        self.device_id = get_device_id()
231        self.adb = "adb -s " + self.device_id
232
233        self.__wait_for_service()
234        self.__init_socket_port()
235
236        self.__close_camera()
237        self.__open_camera()
238        return self
239
240    def __exit__(self, type, value, traceback):
241        if hasattr(self, 'sock') and self.sock:
242            self.__close_camera()
243            self.sock.close()
244        return False
245
246    def __read_response_from_socket(self):
247        # Read a line (newline-terminated) string serialization of JSON object.
248        chars = []
249        while len(chars) == 0 or chars[-1] != '\n':
250            ch = self.sock.recv(1)
251            if len(ch) == 0:
252                # Socket was probably closed; otherwise don't get empty strings
253                raise its.error.Error('Problem with socket on device side')
254            chars.append(ch)
255        line = ''.join(chars)
256        jobj = json.loads(line)
257        # Optionally read a binary buffer of a fixed size.
258        buf = None
259        if jobj.has_key("bufValueSize"):
260            n = jobj["bufValueSize"]
261            buf = bytearray(n)
262            view = memoryview(buf)
263            while n > 0:
264                nbytes = self.sock.recv_into(view, n)
265                view = view[nbytes:]
266                n -= nbytes
267            buf = numpy.frombuffer(buf, dtype=numpy.uint8)
268        return jobj, buf
269
270    def __open_camera(self):
271        # Get the camera ID to open if it is an argument as a single camera.
272        # This allows passing camera=# to individual tests at command line
273        # and camera=#,#,# or an no camera argv with tools/run_all_tests.py.
274        #
275        # In case the camera is a logical multi-camera, to run ITS on the
276        # hidden physical sub-camera, pass camera=[logical ID]:[physical ID]
277        # to an individual test at the command line, and same applies to multiple
278        # camera IDs for tools/run_all_tests.py: camera=#,#:#,#:#,#
279        if not self._camera_id:
280            self._camera_id = 0
281            for s in sys.argv[1:]:
282                if s[:7] == "camera=" and len(s) > 7:
283                    camera_ids = s[7:].split(',')
284                    camera_id_combos = parse_camera_ids(camera_ids)
285                    if len(camera_id_combos) == 1:
286                        self._camera_id = camera_id_combos[0].id
287                        self._hidden_physical_id = camera_id_combos[0].sub_id
288
289        cmd = {"cmdName":"open", "cameraId":self._camera_id}
290        self.sock.send(json.dumps(cmd) + "\n")
291        data,_ = self.__read_response_from_socket()
292        if data['tag'] != 'cameraOpened':
293            raise its.error.Error('Invalid command response')
294
295    def __close_camera(self):
296        cmd = {"cmdName":"close"}
297        self.sock.send(json.dumps(cmd) + "\n")
298        data,_ = self.__read_response_from_socket()
299        if data['tag'] != 'cameraClosed':
300            raise its.error.Error('Invalid command response')
301
302    def do_vibrate(self, pattern):
303        """Cause the device to vibrate to a specific pattern.
304
305        Args:
306            pattern: Durations (ms) for which to turn on or off the vibrator.
307                The first value indicates the number of milliseconds to wait
308                before turning the vibrator on. The next value indicates the
309                number of milliseconds for which to keep the vibrator on
310                before turning it off. Subsequent values alternate between
311                durations in milliseconds to turn the vibrator off or to turn
312                the vibrator on.
313
314        Returns:
315            Nothing.
316        """
317        cmd = {}
318        cmd["cmdName"] = "doVibrate"
319        cmd["pattern"] = pattern
320        self.sock.send(json.dumps(cmd) + "\n")
321        data,_ = self.__read_response_from_socket()
322        if data['tag'] != 'vibrationStarted':
323            raise its.error.Error('Invalid command response')
324
325    def get_sensors(self):
326        """Get all sensors on the device.
327
328        Returns:
329            A Python dictionary that returns keys and booleans for each sensor.
330        """
331        cmd = {}
332        cmd["cmdName"] = "checkSensorExistence"
333        self.sock.send(json.dumps(cmd) + "\n")
334        data,_ = self.__read_response_from_socket()
335        if data['tag'] != 'sensorExistence':
336            raise its.error.Error('Invalid command response')
337        return data['objValue']
338
339    def start_sensor_events(self):
340        """Start collecting sensor events on the device.
341
342        See get_sensor_events for more info.
343
344        Returns:
345            Nothing.
346        """
347        cmd = {}
348        cmd["cmdName"] = "startSensorEvents"
349        self.sock.send(json.dumps(cmd) + "\n")
350        data,_ = self.__read_response_from_socket()
351        if data['tag'] != 'sensorEventsStarted':
352            raise its.error.Error('Invalid command response')
353
354    def get_sensor_events(self):
355        """Get a trace of all sensor events on the device.
356
357        The trace starts when the start_sensor_events function is called. If
358        the test runs for a long time after this call, then the device's
359        internal memory can fill up. Calling get_sensor_events gets all events
360        from the device, and then stops the device from collecting events and
361        clears the internal buffer; to start again, the start_sensor_events
362        call must be used again.
363
364        Events from the accelerometer, compass, and gyro are returned; each
365        has a timestamp and x,y,z values.
366
367        Note that sensor events are only produced if the device isn't in its
368        standby mode (i.e.) if the screen is on.
369
370        Returns:
371            A Python dictionary with three keys ("accel", "mag", "gyro") each
372            of which maps to a list of objects containing "time","x","y","z"
373            keys.
374        """
375        cmd = {}
376        cmd["cmdName"] = "getSensorEvents"
377        self.sock.send(json.dumps(cmd) + "\n")
378        timeout = self.SOCK_TIMEOUT + self.EXTRA_SOCK_TIMEOUT
379        self.sock.settimeout(timeout)
380        data,_ = self.__read_response_from_socket()
381        if data['tag'] != 'sensorEvents':
382            raise its.error.Error('Invalid command response')
383        self.sock.settimeout(self.SOCK_TIMEOUT)
384        return data['objValue']
385
386    def get_camera_ids(self):
387        """Get a list of camera device Ids that can be opened.
388
389        Returns:
390            a list of camera ID string
391        """
392        cmd = {}
393        cmd["cmdName"] = "getCameraIds"
394        self.sock.send(json.dumps(cmd) + "\n")
395        data,_ = self.__read_response_from_socket()
396        if data['tag'] != 'cameraIds':
397            raise its.error.Error('Invalid command response')
398        return data['objValue']['cameraIdArray']
399
400    def check_its_version_compatible(self):
401        """Check the java side ItsService is compatible with current host script.
402           Raise ItsException if versions are incompatible
403
404        Returns: None
405        """
406        cmd = {}
407        cmd["cmdName"] = "getItsVersion"
408        self.sock.send(json.dumps(cmd) + "\n")
409        data,_ = self.__read_response_from_socket()
410        if data['tag'] != 'ItsVersion':
411            raise its.error.Error('ItsService is incompatible with host python script')
412        server_version = data['strValue']
413        if self.ITS_SERVICE_VERSION != server_version:
414            raise its.error.Error('Version mismatch ItsService(%s) vs host script(%s)' % (
415                    server_version, ITS_SERVICE_VERSION))
416
417    def override_with_hidden_physical_camera_props(self, props):
418        """If current session is for a hidden physical camera, check that it is a valid
419           sub-camera backing the logical camera, and return the
420           characteristics of sub-camera. Otherwise, return "props" directly.
421
422        Returns: The properties of the hidden physical camera if possible
423        """
424        if self._hidden_physical_id:
425            e_msg = 'Camera %s is not a logical multi-camera' % self._camera_id
426            assert its.caps.logical_multi_camera(props), e_msg
427            physical_ids = its.caps.logical_multi_camera_physical_ids(props)
428            e_msg = 'Camera %s is not a hidden sub-camera of camera %s' % (
429                self._hidden_physical_id, self._camera_id)
430            assert self._hidden_physical_id in physical_ids, e_msg
431            props = self.get_camera_properties_by_id(self._hidden_physical_id)
432        return props
433
434    def get_camera_properties(self):
435        """Get the camera properties object for the device.
436
437        Returns:
438            The Python dictionary object for the CameraProperties object.
439        """
440        cmd = {}
441        cmd["cmdName"] = "getCameraProperties"
442        self.sock.send(json.dumps(cmd) + "\n")
443        data,_ = self.__read_response_from_socket()
444        if data['tag'] != 'cameraProperties':
445            raise its.error.Error('Invalid command response')
446        self.props = data['objValue']['cameraProperties']
447        return data['objValue']['cameraProperties']
448
449    def get_camera_properties_by_id(self, camera_id):
450        """Get the camera properties object for device with camera_id
451
452        Args:
453            camera_id: The ID string of the camera
454
455        Returns:
456            The Python dictionary object for the CameraProperties object. Empty
457            if no such device exists.
458
459        """
460        cmd = {}
461        cmd["cmdName"] = "getCameraPropertiesById"
462        cmd["cameraId"] = camera_id
463        self.sock.send(json.dumps(cmd) + "\n")
464        data,_ = self.__read_response_from_socket()
465        if data['tag'] != 'cameraProperties':
466            raise its.error.Error('Invalid command response')
467        return data['objValue']['cameraProperties']
468
469    def do_3a(self, regions_ae=[[0,0,1,1,1]],
470                    regions_awb=[[0,0,1,1,1]],
471                    regions_af=[[0,0,1,1,1]],
472                    do_ae=True, do_awb=True, do_af=True,
473                    lock_ae=False, lock_awb=False,
474                    get_results=False,
475                    ev_comp=0, mono_camera=False):
476        """Perform a 3A operation on the device.
477
478        Triggers some or all of AE, AWB, and AF, and returns once they have
479        converged. Uses the vendor 3A that is implemented inside the HAL.
480        Note: do_awb is always enabled regardless of do_awb flag
481
482        Throws an assertion if 3A fails to converge.
483
484        Args:
485            regions_ae: List of weighted AE regions.
486            regions_awb: List of weighted AWB regions.
487            regions_af: List of weighted AF regions.
488            do_ae: Trigger AE and wait for it to converge.
489            do_awb: Wait for AWB to converge.
490            do_af: Trigger AF and wait for it to converge.
491            lock_ae: Request AE lock after convergence, and wait for it.
492            lock_awb: Request AWB lock after convergence, and wait for it.
493            get_results: Return the 3A results from this function.
494            ev_comp: An EV compensation value to use when running AE.
495            mono_camera: Boolean for monochrome camera.
496
497        Region format in args:
498            Arguments are lists of weighted regions; each weighted region is a
499            list of 5 values, [x,y,w,h, wgt], and each argument is a list of
500            these 5-value lists. The coordinates are given as normalized
501            rectangles (x,y,w,h) specifying the region. For example:
502                [[0.0, 0.0, 1.0, 0.5, 5], [0.0, 0.5, 1.0, 0.5, 10]].
503            Weights are non-negative integers.
504
505        Returns:
506            Five values are returned if get_results is true::
507            * AE sensitivity; None if do_ae is False
508            * AE exposure time; None if do_ae is False
509            * AWB gains (list);
510            * AWB transform (list);
511            * AF focus position; None if do_af is false
512            Otherwise, it returns five None values.
513        """
514        print "Running vendor 3A on device"
515        cmd = {}
516        cmd["cmdName"] = "do3A"
517        cmd["regions"] = {"ae": sum(regions_ae, []),
518                          "awb": sum(regions_awb, []),
519                          "af": sum(regions_af, [])}
520        cmd["triggers"] = {"ae": do_ae, "af": do_af}
521        if lock_ae:
522            cmd["aeLock"] = True
523        if lock_awb:
524            cmd["awbLock"] = True
525        if ev_comp != 0:
526            cmd["evComp"] = ev_comp
527        if self._hidden_physical_id:
528            cmd["physicalId"] = self._hidden_physical_id
529        self.sock.send(json.dumps(cmd) + "\n")
530
531        # Wait for each specified 3A to converge.
532        ae_sens = None
533        ae_exp = None
534        awb_gains = None
535        awb_transform = None
536        af_dist = None
537        converged = False
538        while True:
539            data,_ = self.__read_response_from_socket()
540            vals = data['strValue'].split()
541            if data['tag'] == 'aeResult':
542                if do_ae:
543                    ae_sens, ae_exp = [int(i) for i in vals]
544            elif data['tag'] == 'afResult':
545                if do_af:
546                    af_dist = float(vals[0])
547            elif data['tag'] == 'awbResult':
548                awb_gains = [float(f) for f in vals[:4]]
549                awb_transform = [float(f) for f in vals[4:]]
550            elif data['tag'] == '3aConverged':
551                converged = True
552            elif data['tag'] == '3aDone':
553                break
554            else:
555                raise its.error.Error('Invalid command response')
556        if converged and not get_results:
557            return None,None,None,None,None
558        if (do_ae and ae_sens == None or (not mono_camera and do_awb and awb_gains == None)
559                or do_af and af_dist == None or not converged):
560
561            raise its.error.Error('3A failed to converge')
562        return ae_sens, ae_exp, awb_gains, awb_transform, af_dist
563
564    def is_stream_combination_supported(self, out_surfaces):
565        """Query whether a output surfaces combination is supported by the camera device.
566
567        This function hooks up to the isSessionConfigurationSupported() camera API
568        to query whether a particular stream combination is supported.
569
570        Refer to do_capture function for specification of out_surfaces field.
571        """
572        cmd = {}
573        cmd['cmdName'] = 'isStreamCombinationSupported'
574
575        if not isinstance(out_surfaces, list):
576            cmd['outputSurfaces'] = [out_surfaces]
577        else:
578            cmd['outputSurfaces'] = out_surfaces
579        formats = [c['format'] if 'format' in c else 'yuv'
580                   for c in cmd['outputSurfaces']]
581        formats = [s if s != 'jpg' else 'jpeg' for s in formats]
582
583        self.sock.send(json.dumps(cmd) + '\n')
584
585        data,_ = self.__read_response_from_socket()
586        if data['tag'] != 'streamCombinationSupport':
587            its.error.Error('Failed to query stream combination')
588
589        return data['strValue'] == 'supportedCombination'
590
591    def do_capture(self, cap_request,
592            out_surfaces=None, reprocess_format=None, repeat_request=None):
593        """Issue capture request(s), and read back the image(s) and metadata.
594
595        The main top-level function for capturing one or more images using the
596        device. Captures a single image if cap_request is a single object, and
597        captures a burst if it is a list of objects.
598
599        The optional repeat_request field can be used to assign a repeating
600        request list ran in background for 3 seconds to warm up the capturing
601        pipeline before start capturing. The repeat_requests will be ran on a
602        640x480 YUV surface without sending any data back. The caller needs to
603        make sure the stream configuration defined by out_surfaces and
604        repeat_request are valid or do_capture may fail because device does not
605        support such stream configuration.
606
607        The out_surfaces field can specify the width(s), height(s), and
608        format(s) of the captured image. The formats may be "yuv", "jpeg",
609        "dng", "raw", "raw10", "raw12", "rawStats" or "y8". The default is a YUV420
610        frame ("yuv") corresponding to a full sensor frame.
611
612        Optionally the out_surfaces field can specify physical camera id(s) if the
613        current camera device is a logical multi-camera. The physical camera id
614        must refer to a physical camera backing this logical camera device.
615
616        Note that one or more surfaces can be specified, allowing a capture to
617        request images back in multiple formats (e.g.) raw+yuv, raw+jpeg,
618        yuv+jpeg, raw+yuv+jpeg. If the size is omitted for a surface, the
619        default is the largest resolution available for the format of that
620        surface. At most one output surface can be specified for a given format,
621        and raw+dng, raw10+dng, and raw+raw10 are not supported as combinations.
622
623        If reprocess_format is not None, for each request, an intermediate
624        buffer of the given reprocess_format will be captured from camera and
625        the intermediate buffer will be reprocessed to the output surfaces. The
626        following settings will be turned off when capturing the intermediate
627        buffer and will be applied when reprocessing the intermediate buffer.
628            1. android.noiseReduction.mode
629            2. android.edge.mode
630            3. android.reprocess.effectiveExposureFactor
631
632        Supported reprocess format are "yuv" and "private". Supported output
633        surface formats when reprocessing is enabled are "yuv" and "jpeg".
634
635        Example of a single capture request:
636
637            {
638                "android.sensor.exposureTime": 100*1000*1000,
639                "android.sensor.sensitivity": 100
640            }
641
642        Example of a list of capture requests:
643
644            [
645                {
646                    "android.sensor.exposureTime": 100*1000*1000,
647                    "android.sensor.sensitivity": 100
648                },
649                {
650                    "android.sensor.exposureTime": 100*1000*1000,
651                    "android.sensor.sensitivity": 200
652                }
653            ]
654
655        Examples of output surface specifications:
656
657            {
658                "width": 640,
659                "height": 480,
660                "format": "yuv"
661            }
662
663            [
664                {
665                    "format": "jpeg"
666                },
667                {
668                    "format": "raw"
669                }
670            ]
671
672        The following variables defined in this class are shortcuts for
673        specifying one or more formats where each output is the full size for
674        that format; they can be used as values for the out_surfaces arguments:
675
676            CAP_RAW
677            CAP_DNG
678            CAP_YUV
679            CAP_JPEG
680            CAP_RAW_YUV
681            CAP_DNG_YUV
682            CAP_RAW_JPEG
683            CAP_DNG_JPEG
684            CAP_YUV_JPEG
685            CAP_RAW_YUV_JPEG
686            CAP_DNG_YUV_JPEG
687
688        If multiple formats are specified, then this function returns multiple
689        capture objects, one for each requested format. If multiple formats and
690        multiple captures (i.e. a burst) are specified, then this function
691        returns multiple lists of capture objects. In both cases, the order of
692        the returned objects matches the order of the requested formats in the
693        out_surfaces parameter. For example:
694
695            yuv_cap            = do_capture( req1                           )
696            yuv_cap            = do_capture( req1,        yuv_fmt           )
697            yuv_cap,  raw_cap  = do_capture( req1,        [yuv_fmt,raw_fmt] )
698            yuv_caps           = do_capture( [req1,req2], yuv_fmt           )
699            yuv_caps, raw_caps = do_capture( [req1,req2], [yuv_fmt,raw_fmt] )
700
701        The "rawStats" format processes the raw image and returns a new image
702        of statistics from the raw image. The format takes additional keys,
703        "gridWidth" and "gridHeight" which are size of grid cells in a 2D grid
704        of the raw image. For each grid cell, the mean and variance of each raw
705        channel is computed, and the do_capture call returns two 4-element float
706        images of dimensions (rawWidth / gridWidth, rawHeight / gridHeight),
707        concatenated back-to-back, where the first iamge contains the 4-channel
708        means and the second contains the 4-channel variances. Note that only
709        pixels in the active array crop region are used; pixels outside this
710        region (for example optical black rows) are cropped out before the
711        gridding and statistics computation is performed.
712
713        For the rawStats format, if the gridWidth is not provided then the raw
714        image width is used as the default, and similarly for gridHeight. With
715        this, the following is an example of a output description that computes
716        the mean and variance across each image row:
717
718            {
719                "gridHeight": 1,
720                "format": "rawStats"
721            }
722
723        Args:
724            cap_request: The Python dict/list specifying the capture(s), which
725                will be converted to JSON and sent to the device.
726            out_surfaces: (Optional) specifications of the output image formats
727                and sizes to use for each capture.
728            reprocess_format: (Optional) The reprocessing format. If not None,
729                reprocessing will be enabled.
730
731        Returns:
732            An object, list of objects, or list of lists of objects, where each
733            object contains the following fields:
734            * data: the image data as a numpy array of bytes.
735            * width: the width of the captured image.
736            * height: the height of the captured image.
737            * format: image the format, in [
738                        "yuv","jpeg","raw","raw10","raw12","rawStats","dng"].
739            * metadata: the capture result object (Python dictionary).
740        """
741        cmd = {}
742        if reprocess_format != None:
743            cmd["cmdName"] = "doReprocessCapture"
744            cmd["reprocessFormat"] = reprocess_format
745        else:
746            cmd["cmdName"] = "doCapture"
747
748        if repeat_request is not None and reprocess_format is not None:
749            raise its.error.Error('repeating request + reprocessing is not supported')
750
751        if repeat_request is None:
752            cmd["repeatRequests"] = []
753        elif not isinstance(repeat_request, list):
754            cmd["repeatRequests"] = [repeat_request]
755        else:
756            cmd["repeatRequests"] = repeat_request
757
758        if not isinstance(cap_request, list):
759            cmd["captureRequests"] = [cap_request]
760        else:
761            cmd["captureRequests"] = cap_request
762        if out_surfaces is not None:
763            if not isinstance(out_surfaces, list):
764                cmd["outputSurfaces"] = [out_surfaces]
765            else:
766                cmd["outputSurfaces"] = out_surfaces
767            formats = [c["format"] if "format" in c else "yuv"
768                       for c in cmd["outputSurfaces"]]
769            formats = [s if s != "jpg" else "jpeg" for s in formats]
770        else:
771            max_yuv_size = its.objects.get_available_output_sizes(
772                    "yuv", self.props)[0]
773            formats = ['yuv']
774            cmd["outputSurfaces"] = [{"format": "yuv",
775                                      "width" : max_yuv_size[0],
776                                      "height": max_yuv_size[1]}]
777
778        ncap = len(cmd["captureRequests"])
779        nsurf = 1 if out_surfaces is None else len(cmd["outputSurfaces"])
780
781        cam_ids = []
782        bufs = {}
783        yuv_bufs = {}
784        for i,s in enumerate(cmd["outputSurfaces"]):
785            if self._hidden_physical_id:
786                s['physicalCamera'] = self._hidden_physical_id
787
788            if 'physicalCamera' in s:
789                cam_id = s['physicalCamera']
790            else:
791                cam_id = self._camera_id
792
793            if cam_id not in cam_ids:
794                cam_ids.append(cam_id)
795                bufs[cam_id] = {"raw":[], "raw10":[], "raw12":[],
796                        "rawStats":[], "dng":[], "jpeg":[], "y8":[]}
797
798        for cam_id in cam_ids:
799            # Only allow yuv output to multiple targets
800            if cam_id == self._camera_id:
801                yuv_surfaces = [s for s in cmd["outputSurfaces"] if s["format"]=="yuv"\
802                                and "physicalCamera" not in s]
803                formats_for_id = [s["format"] for s in cmd["outputSurfaces"] if \
804                                 "physicalCamera" not in s]
805            else:
806                yuv_surfaces = [s for s in cmd["outputSurfaces"] if s["format"]=="yuv"\
807                                and "physicalCamera" in s and s["physicalCamera"] == cam_id]
808                formats_for_id = [s["format"] for s in cmd["outputSurfaces"] if \
809                                 "physicalCamera" in s and s["physicalCamera"] == cam_id]
810
811            n_yuv = len(yuv_surfaces)
812            # Compute the buffer size of YUV targets
813            yuv_maxsize_1d = 0
814            for s in yuv_surfaces:
815                if not ("width" in s and "height" in s):
816                    if self.props is None:
817                        raise its.error.Error('Camera props are unavailable')
818                    yuv_maxsize_2d = its.objects.get_available_output_sizes(
819                        "yuv", self.props)[0]
820                    yuv_maxsize_1d = yuv_maxsize_2d[0] * yuv_maxsize_2d[1] * 3 / 2
821                    break
822            yuv_sizes = [c["width"]*c["height"]*3/2
823                         if "width" in c and "height" in c
824                         else yuv_maxsize_1d
825                         for c in yuv_surfaces]
826            # Currently we don't pass enough metadta from ItsService to distinguish
827            # different yuv stream of same buffer size
828            if len(yuv_sizes) != len(set(yuv_sizes)):
829                raise its.error.Error(
830                        'ITS does not support yuv outputs of same buffer size')
831            if len(formats_for_id) > len(set(formats_for_id)):
832                if n_yuv != len(formats_for_id) - len(set(formats_for_id)) + 1:
833                    raise its.error.Error('Duplicate format requested')
834
835            yuv_bufs[cam_id] = {size:[] for size in yuv_sizes}
836
837        raw_formats = 0;
838        raw_formats += 1 if "dng" in formats else 0
839        raw_formats += 1 if "raw" in formats else 0
840        raw_formats += 1 if "raw10" in formats else 0
841        raw_formats += 1 if "raw12" in formats else 0
842        raw_formats += 1 if "rawStats" in formats else 0
843        if raw_formats > 1:
844            raise its.error.Error('Different raw formats not supported')
845
846        # Detect long exposure time and set timeout accordingly
847        longest_exp_time = 0
848        for req in cmd["captureRequests"]:
849            if "android.sensor.exposureTime" in req and \
850                    req["android.sensor.exposureTime"] > longest_exp_time:
851                longest_exp_time = req["android.sensor.exposureTime"]
852
853        extended_timeout = longest_exp_time / self.SEC_TO_NSEC + \
854                self.SOCK_TIMEOUT
855        if repeat_request:
856            extended_timeout += self.EXTRA_SOCK_TIMEOUT
857        self.sock.settimeout(extended_timeout)
858
859        print "Capturing %d frame%s with %d format%s [%s]" % (
860                  ncap, "s" if ncap>1 else "", nsurf, "s" if nsurf>1 else "",
861                  ",".join(formats))
862        self.sock.send(json.dumps(cmd) + "\n")
863
864        # Wait for ncap*nsurf images and ncap metadata responses.
865        # Assume that captures come out in the same order as requested in
866        # the burst, however individual images of different formats can come
867        # out in any order for that capture.
868        nbufs = 0
869        mds = []
870        physical_mds = []
871        widths = None
872        heights = None
873        while nbufs < ncap*nsurf or len(mds) < ncap:
874            jsonObj,buf = self.__read_response_from_socket()
875            if jsonObj['tag'] in ['jpegImage', 'rawImage', \
876                    'raw10Image', 'raw12Image', 'rawStatsImage', 'dngImage', 'y8Image'] \
877                    and buf is not None:
878                fmt = jsonObj['tag'][:-5]
879                bufs[self._camera_id][fmt].append(buf)
880                nbufs += 1
881            elif jsonObj['tag'] == 'yuvImage':
882                buf_size = numpy.product(buf.shape)
883                yuv_bufs[self._camera_id][buf_size].append(buf)
884                nbufs += 1
885            elif jsonObj['tag'] == 'captureResults':
886                mds.append(jsonObj['objValue']['captureResult'])
887                physical_mds.append(jsonObj['objValue']['physicalResults'])
888                outputs = jsonObj['objValue']['outputs']
889                widths = [out['width'] for out in outputs]
890                heights = [out['height'] for out in outputs]
891            else:
892                tagString = unicodedata.normalize('NFKD', jsonObj['tag']).encode('ascii', 'ignore');
893                for x in ['jpegImage', 'rawImage', \
894                        'raw10Image', 'raw12Image', 'rawStatsImage', 'yuvImage']:
895                    if tagString.startswith(x):
896                        if x == 'yuvImage':
897                            physicalId = jsonObj['tag'][len(x):]
898                            if physicalId in cam_ids:
899                                buf_size = numpy.product(buf.shape)
900                                yuv_bufs[physicalId][buf_size].append(buf)
901                                nbufs += 1
902                        else:
903                            physicalId = jsonObj['tag'][len(x):]
904                            if physicalId in cam_ids:
905                                fmt = x[:-5]
906                                bufs[physicalId][fmt].append(buf)
907                                nbufs += 1
908        rets = []
909        for j,fmt in enumerate(formats):
910            objs = []
911            if "physicalCamera" in cmd["outputSurfaces"][j]:
912                cam_id = cmd["outputSurfaces"][j]["physicalCamera"]
913            else:
914                cam_id = self._camera_id
915
916            for i in range(ncap):
917                obj = {}
918                obj["width"] = widths[j]
919                obj["height"] = heights[j]
920                obj["format"] = fmt
921                if cam_id == self._camera_id:
922                    obj["metadata"] = mds[i]
923                else:
924                    for physical_md in physical_mds[i]:
925                        if cam_id in physical_md:
926                            obj["metadata"] = physical_md[cam_id]
927                            break
928
929                if fmt == "yuv":
930                    buf_size = widths[j] * heights[j] * 3 / 2
931                    obj["data"] = yuv_bufs[cam_id][buf_size][i]
932                else:
933                    obj["data"] = bufs[cam_id][fmt][i]
934                objs.append(obj)
935            rets.append(objs if ncap > 1 else objs[0])
936        self.sock.settimeout(self.SOCK_TIMEOUT)
937        if len(rets) > 1 or (isinstance(rets[0], dict) and
938                             isinstance(cap_request, list)):
939            return rets
940        else:
941            return rets[0]
942
943def do_capture_with_latency(cam, req, sync_latency, fmt=None):
944    """Helper function to take enough frames with do_capture to allow sync latency.
945
946    Args:
947        cam:            camera object
948        req:            request for camera
949        sync_latency:   integer number of frames
950        fmt:            format for the capture
951    Returns:
952        single capture with the unsettled frames discarded
953    """
954    caps = cam.do_capture([req]*(sync_latency+1), fmt)
955    return caps[-1]
956
957
958def get_device_id():
959    """Return the ID of the device that the test is running on.
960
961    Return the device ID provided in the command line if it's connected. If no
962    device ID is provided in the command line and there is only one device
963    connected, return the device ID by parsing the result of "adb devices".
964    Also, if the environment variable ANDROID_SERIAL is set, use it as device
965    id. When both ANDROID_SERIAL and device argument present, device argument
966    takes priority.
967
968    Raise an exception if no device is connected; or the device ID provided in
969    the command line is not connected; or no device ID is provided in the
970    command line or environment variable and there are more than 1 device
971    connected.
972
973    Returns:
974        Device ID string.
975    """
976    device_id = None
977
978    # Check if device id is set in env
979    if "ANDROID_SERIAL" in os.environ:
980        device_id = os.environ["ANDROID_SERIAL"]
981
982    for s in sys.argv[1:]:
983        if s[:7] == "device=" and len(s) > 7:
984            device_id = str(s[7:])
985
986    # Get a list of connected devices
987    devices = []
988    command = "adb devices"
989    proc = subprocess.Popen(command.split(), stdout=subprocess.PIPE)
990    output, error = proc.communicate()
991    for line in output.split(os.linesep):
992        device_info = line.split()
993        if len(device_info) == 2 and device_info[1] == "device":
994            devices.append(device_info[0])
995
996    if len(devices) == 0:
997        raise its.error.Error("No device is connected!")
998    elif device_id is not None and device_id not in devices:
999        raise its.error.Error(device_id + " is not connected!")
1000    elif device_id is None and len(devices) >= 2:
1001        raise its.error.Error("More than 1 device are connected. " +
1002                "Use device=<device_id> to specify a device to test.")
1003    elif len(devices) == 1:
1004        device_id = devices[0]
1005
1006    return device_id
1007
1008def report_result(device_id, camera_id, results):
1009    """Send a pass/fail result to the device, via an intent.
1010
1011    Args:
1012        device_id: The ID string of the device to report the results to.
1013        camera_id: The ID string of the camera for which to report pass/fail.
1014        results: a dictionary contains all ITS scenes as key and result/summary
1015                 of current ITS run. See test_report_result unit test for
1016                 an example.
1017    Returns:
1018        Nothing.
1019    """
1020    ACTIVITY_START_WAIT = 1.5 # seconds
1021    adb = "adb -s " + device_id
1022
1023    # Start ItsTestActivity to receive test results
1024    cmd = "%s shell am start %s --activity-brought-to-front" % (adb, ItsSession.ITS_TEST_ACTIVITY)
1025    _run(cmd)
1026    time.sleep(ACTIVITY_START_WAIT)
1027
1028    # Validate/process results argument
1029    for scene in results:
1030        result_key = ItsSession.RESULT_KEY
1031        summary_key = ItsSession.SUMMARY_KEY
1032        if result_key not in results[scene]:
1033            raise its.error.Error('ITS result not found for ' + scene)
1034        if results[scene][result_key] not in ItsSession.RESULT_VALUES:
1035            raise its.error.Error('Unknown ITS result for %s: %s' % (
1036                    scene, results[result_key]))
1037        if summary_key in results[scene]:
1038            device_summary_path = "/sdcard/its_camera%s_%s.txt" % (
1039                    camera_id, scene)
1040            _run("%s push %s %s" % (
1041                    adb, results[scene][summary_key], device_summary_path))
1042            results[scene][summary_key] = device_summary_path
1043
1044    json_results = json.dumps(results)
1045    cmd = "%s shell am broadcast -a %s --es %s %s --es %s %s --es %s \'%s\'" % (
1046            adb, ItsSession.ACTION_ITS_RESULT,
1047            ItsSession.EXTRA_VERSION, ItsSession.CURRENT_ITS_VERSION,
1048            ItsSession.EXTRA_CAMERA_ID, camera_id,
1049            ItsSession.EXTRA_RESULTS, json_results)
1050    if len(cmd) > 4095:
1051        print "ITS command string might be too long! len:", len(cmd)
1052    _run(cmd)
1053
1054def adb_log(device_id, msg):
1055    """Send a log message to adb logcat
1056
1057    Args:
1058        device_id: The ID string of the adb device
1059        msg: the message string to be send to logcat
1060
1061    Returns:
1062        Nothing.
1063    """
1064    adb = "adb -s " + device_id
1065    cmd = "%s shell log -p i -t \"ItsTestHost\" %s" % (adb, msg)
1066    _run(cmd)
1067
1068def get_device_fingerprint(device_id):
1069    """ Return the Build FingerPrint of the device that the test is running on.
1070
1071    Returns:
1072        Device Build Fingerprint string.
1073    """
1074    device_bfp = None
1075
1076    # Get a list of connected devices
1077
1078    com = ('adb -s %s shell getprop | grep ro.build.fingerprint' % device_id)
1079    proc = subprocess.Popen(com.split(), stdout=subprocess.PIPE)
1080    output, error = proc.communicate()
1081    assert error is None
1082
1083    lst = string.split( \
1084            string.replace( \
1085            string.replace( \
1086            string.replace(output,
1087            '\n', ''), '[', ''), ']', ''), \
1088            ' ')
1089
1090    if lst[0].find('ro.build.fingerprint') != -1:
1091        device_bfp = lst[1]
1092
1093    return device_bfp
1094
1095def parse_camera_ids(ids):
1096    """ Parse the string of camera IDs into array of CameraIdCombo tuples.
1097    """
1098    CameraIdCombo = namedtuple('CameraIdCombo', ['id', 'sub_id'])
1099    id_combos = []
1100    for one_id in ids:
1101        one_combo = one_id.split(':')
1102        if len(one_combo) == 1:
1103            id_combos.append(CameraIdCombo(one_combo[0], None))
1104        elif len(one_combo) == 2:
1105            id_combos.append(CameraIdCombo(one_combo[0], one_combo[1]))
1106        else:
1107            assert(False), 'Camera id parameters must be either ID, or ID:SUB_ID'
1108    return id_combos
1109
1110def _run(cmd):
1111    """Replacement for os.system, with hiding of stdout+stderr messages.
1112    """
1113    with open(os.devnull, 'wb') as devnull:
1114        subprocess.check_call(
1115                cmd.split(), stdout=devnull, stderr=subprocess.STDOUT)
1116
1117
1118class __UnitTest(unittest.TestCase):
1119    """Run a suite of unit tests on this module.
1120    """
1121
1122    """
1123    # TODO: this test currently needs connected device to pass
1124    #       Need to remove that dependency before enabling the test
1125    def test_report_result(self):
1126        device_id = get_device_id()
1127        camera_id = "1"
1128        result_key = ItsSession.RESULT_KEY
1129        results = {"scene0":{result_key:"PASS"},
1130                   "scene1":{result_key:"PASS"},
1131                   "scene2":{result_key:"PASS"},
1132                   "scene3":{result_key:"PASS"},
1133                   "sceneNotExist":{result_key:"FAIL"}}
1134        report_result(device_id, camera_id, results)
1135    """
1136
1137if __name__ == '__main__':
1138    unittest.main()
1139
1140