Merge "ITS: Enable hidden physical camera ITS test" into qt-dev
diff --git a/apps/CameraITS/pymodules/its/device.py b/apps/CameraITS/pymodules/its/device.py
index f8aad38..9b837cd 100644
--- a/apps/CameraITS/pymodules/its/device.py
+++ b/apps/CameraITS/pymodules/its/device.py
@@ -25,6 +25,7 @@
 import its.error
 import numpy
 
+from collections import namedtuple
 
 class ItsSession(object):
     """Controls a device over adb to run ITS scripts.
@@ -218,8 +219,9 @@
                 break
         proc.kill()
 
-    def __init__(self, camera_id=None):
+    def __init__(self, camera_id=None, hidden_physical_id=None):
         self._camera_id = camera_id
+        self._hidden_physical_id = hidden_physical_id
 
     def __enter__(self):
         # Initialize device id and adb command.
@@ -230,7 +232,7 @@
         self.__init_socket_port()
 
         self.__close_camera()
-        self.__open_camera(self._camera_id)
+        self.__open_camera()
         return self
 
     def __exit__(self, type, value, traceback):
@@ -263,18 +265,26 @@
             buf = numpy.frombuffer(buf, dtype=numpy.uint8)
         return jobj, buf
 
-    def __open_camera(self, camera_id):
+    def __open_camera(self):
         # Get the camera ID to open if it is an argument as a single camera.
         # This allows passing camera=# to individual tests at command line
         # and camera=#,#,# or an no camera argv with tools/run_all_tests.py.
-        if not camera_id:
-            camera_id = 0
+        #
+        # In case the camera is a logical multi-camera, to run ITS on the
+        # hidden physical sub-camera, pass camera=[logical ID]:[physical ID]
+        # to an individual test at the command line, and same applies to multiple
+        # camera IDs for tools/run_all_tests.py: camera=#,#:#,#:#,#
+        if not self._camera_id:
+            self._camera_id = 0
             for s in sys.argv[1:]:
                 if s[:7] == "camera=" and len(s) > 7:
-                    camera_ids = s[7:].split(",")
-                    if len(camera_ids) == 1:
-                        camera_id = camera_ids[0]
-        cmd = {"cmdName":"open", "cameraId":camera_id}
+                    camera_ids = s[7:].split(',')
+                    camera_id_combos = parse_camera_ids(camera_ids)
+                    if len(camera_id_combos) == 1:
+                        self._camera_id = camera_id_combos[0].id
+                        self._hidden_physical_id = camera_id_combos[0].sub_id
+
+        cmd = {"cmdName":"open", "cameraId":self._camera_id}
         self.sock.send(json.dumps(cmd) + "\n")
         data,_ = self.__read_response_from_socket()
         if data['tag'] != 'cameraOpened':
@@ -402,6 +412,23 @@
             raise its.error.Error('Version mismatch ItsService(%s) vs host script(%s)' % (
                     server_version, ITS_SERVICE_VERSION))
 
+    def override_with_hidden_physical_camera_props(self, props):
+        """If current session is for a hidden physical camera, check that it is a valid
+           sub-camera backing the logical camera, and return the
+           characteristics of sub-camera. Otherwise, return "props" directly.
+
+        Returns: The properties of the hidden physical camera if possible
+        """
+        if self._hidden_physical_id:
+            e_msg = 'Camera %s is not a logical multi-camera' % self._camera_id
+            assert its.caps.logical_multi_camera(props), e_msg
+            physical_ids = its.caps.logical_multi_camera_physical_ids(props)
+            e_msg = 'Camera %s is not a hidden sub-camera of camera %s' % (
+                self._hidden_physical_id, self._camera_id)
+            assert self._hidden_physical_id in physical_ids, e_msg
+            props = self.get_camera_properties_by_id(self._hidden_physical_id)
+        return props
+
     def get_camera_properties(self):
         """Get the camera properties object for the device.
 
@@ -758,6 +785,9 @@
         physical_cam_format = None
         logical_cam_formats = []
         for i,s in enumerate(cmd["outputSurfaces"]):
+            if self._hidden_physical_id:
+                s['physicalCamera'] = self._hidden_physical_id
+
             if "format" in s and s["format"] in ["yuv", "raw", "raw10", "raw12"]:
                 if "physicalCamera" in s:
                     if physical_cam_format is not None and s["format"] != physical_cam_format:
@@ -1050,6 +1080,20 @@
 
     return device_bfp
 
+def parse_camera_ids(ids):
+    """ Parse the string of camera IDs into array of CameraIdCombo tuples.
+    """
+    CameraIdCombo = namedtuple('CameraIdCombo', ['id', 'sub_id'])
+    id_combos = []
+    for one_id in ids:
+        one_combo = one_id.split(':')
+        if len(one_combo) == 1:
+            id_combos.append(CameraIdCombo(one_combo[0], None))
+        elif len(one_combo) == 2:
+            id_combos.append(CameraIdCombo(one_combo[0], one_combo[1]))
+        else:
+            assert(False), 'Camera id parameters must be either ID, or ID:SUB_ID'
+    return id_combos
 
 def _run(cmd):
     """Replacement for os.system, with hiding of stdout+stderr messages.
diff --git a/apps/CameraITS/tests/scene0/test_read_write.py b/apps/CameraITS/tests/scene0/test_read_write.py
index 357bd02..0f8a7a6 100644
--- a/apps/CameraITS/tests/scene0/test_read_write.py
+++ b/apps/CameraITS/tests/scene0/test_read_write.py
@@ -29,6 +29,7 @@
 
     with its.device.ItsSession() as cam:
         props = cam.get_camera_properties()
+        props = cam.override_with_hidden_physical_camera_props(props)
         its.caps.skip_unless(its.caps.manual_sensor(props) and
                              its.caps.per_frame_control(props))
 
@@ -59,7 +60,8 @@
             print 'format: %s' % fmt
             size = its.objects.get_available_output_sizes(fmt, props)[-1]
             out_surface = {'width': size[0], 'height': size[1], 'format': fmt}
-
+            if cam._hidden_physical_id:
+                out_surface['physicalCamera'] = cam._hidden_physical_id
             reqs = []
             index_list = []
             for exp in exp_range:
diff --git a/apps/CameraITS/tests/scene1/test_dng_noise_model.py b/apps/CameraITS/tests/scene1/test_dng_noise_model.py
index c60f71c..ba8fd7d 100644
--- a/apps/CameraITS/tests/scene1/test_dng_noise_model.py
+++ b/apps/CameraITS/tests/scene1/test_dng_noise_model.py
@@ -39,14 +39,15 @@
     # (since ITS doesn't require a perfectly uniformly lit scene).
 
     with its.device.ItsSession() as cam:
-
         props = cam.get_camera_properties()
+        props = cam.override_with_hidden_physical_camera_props(props)
         its.caps.skip_unless(its.caps.raw(props) and
-                             its.caps.raw16(props) and
-                             its.caps.manual_sensor(props) and
-                             its.caps.read_3a(props) and
-                             its.caps.per_frame_control(props) and
-                             not its.caps.mono_camera(props))
+                its.caps.raw16(props) and
+                its.caps.manual_sensor(props) and
+                its.caps.read_3a(props) and
+                its.caps.per_frame_control(props) and
+                not its.caps.mono_camera(props))
+
         debug = its.caps.debug_mode()
 
         white_level = float(props['android.sensor.info.whiteLevel'])
diff --git a/apps/CameraITS/tests/sensor_fusion/test_sensor_fusion.py b/apps/CameraITS/tests/sensor_fusion/test_sensor_fusion.py
index 77f729e..25296b6 100644
--- a/apps/CameraITS/tests/sensor_fusion/test_sensor_fusion.py
+++ b/apps/CameraITS/tests/sensor_fusion/test_sensor_fusion.py
@@ -429,6 +429,7 @@
     """
     with its.device.ItsSession() as cam:
         props = cam.get_camera_properties()
+        props = cam.override_with_hidden_physical_camera_props(props)
         its.caps.skip_unless(its.caps.read_3a and
                              its.caps.sensor_fusion(props) and
                              props["android.lens.facing"] != FACING_EXTERNAL and
diff --git a/apps/CameraITS/tools/dng_noise_model.py b/apps/CameraITS/tools/dng_noise_model.py
index f35c193..b490d19 100644
--- a/apps/CameraITS/tools/dng_noise_model.py
+++ b/apps/CameraITS/tools/dng_noise_model.py
@@ -87,6 +87,7 @@
 
     with its.device.ItsSession() as cam:
         props = cam.get_camera_properties()
+        props = cam.override_with_hidden_physical_camera_props(props)
 
         # Get basic properties we need.
         sens_min, sens_max = props['android.sensor.info.sensitivityRange']
diff --git a/apps/CameraITS/tools/run_all_tests.py b/apps/CameraITS/tools/run_all_tests.py
index 26a2c8d..0cdd008 100644
--- a/apps/CameraITS/tools/run_all_tests.py
+++ b/apps/CameraITS/tools/run_all_tests.py
@@ -72,6 +72,24 @@
         'sensor_fusion': []
 }
 
+# Must match mHiddenPhysicalCameraSceneIds in ItsTestActivity.java
+HIDDEN_PHYSICAL_CAMERA_TESTS = {
+        'scene0': [
+                'test_read_write'
+        ],
+        'scene1': [
+                'test_dng_noise_model'
+        ],
+        'scene2': [],
+        'scene2b': [],
+        'scene2c': [],
+        'scene3': [],
+        'scene4': [],
+        'scene5': [],
+        'sensor_fusion': [
+                'test_sensor_fusion'
+        ]
+}
 
 def run_subprocess_with_timeout(cmd, fout, ferr, outdir):
     """Run subprocess with a timeout.
@@ -103,10 +121,11 @@
         return test_code
 
 
-def calc_camera_fov(camera_id):
+def calc_camera_fov(camera_id, hidden_physical_id):
     """Determine the camera field of view from internal params."""
-    with ItsSession(camera_id) as cam:
+    with ItsSession(camera_id, hidden_physical_id) as cam:
         props = cam.get_camera_properties()
+        props = cam.override_with_hidden_physical_camera_props(props)
         focal_ls = props['android.lens.info.availableFocalLengths']
         if len(focal_ls) > 1:
             print 'Doing capture to determine logical camera focal length'
@@ -204,7 +223,7 @@
         "scene5": ["doAF=False"]
     }
 
-    camera_ids = []
+    camera_id_combos = []
     scenes = []
     chart_host_id = None
     result_device_id = None
@@ -213,10 +232,13 @@
     skip_scene_validation = False
     chart_distance = CHART_DISTANCE
     chart_level = CHART_LEVEL
+    one_camera_argv = sys.argv[1:]
 
-    for s in sys.argv[1:]:
+    for s in list(sys.argv[1:]):
         if s[:7] == "camera=" and len(s) > 7:
             camera_ids = s[7:].split(',')
+            camera_id_combos = its.device.parse_camera_ids(camera_ids)
+            one_camera_argv.remove(s)
         elif s[:7] == "scenes=" and len(s) > 7:
             scenes = s[7:].split(',')
         elif s[:6] == 'chart=' and len(s) > 6:
@@ -342,11 +364,12 @@
         assert device_bfp == result_device_bfp, assert_err_msg
 
     # user doesn't specify camera id, run through all cameras
-    if not camera_ids:
+    if not camera_id_combos:
         with its.device.ItsSession() as cam:
             camera_ids = cam.get_camera_ids()
+            camera_id_combos = its.device.parse_camera_ids(camera_ids);
 
-    print "Running ITS on camera: %s, scene %s" % (camera_ids, scenes)
+    print "Running ITS on camera: %s, scene %s" % (camera_id_combos, scenes)
 
     if auto_scene_switch:
         # merge_result only supports run_parallel_tests
@@ -362,15 +385,20 @@
             wake_code = subprocess.call(cmd)
             assert wake_code == 0
 
-    for camera_id in camera_ids:
-        camera_fov = calc_camera_fov(camera_id)
+    for id_combo in camera_id_combos:
+        camera_fov = calc_camera_fov(id_combo.id, id_combo.sub_id)
+        id_combo_string = id_combo.id;
+        has_hidden_sub_camera = id_combo.sub_id is not None
+        if has_hidden_sub_camera:
+            id_combo_string += ":" + id_combo.sub_id
+            scenes = [scene for scene in scenes if HIDDEN_PHYSICAL_CAMERA_TESTS[scene]]
         # Loop capturing images until user confirm test scene is correct
-        camera_id_arg = "camera=" + camera_id
-        print "Preparing to run ITS on camera", camera_id
+        camera_id_arg = "camera=" + id_combo.id
+        print "Preparing to run ITS on camera", id_combo_string, "for scenes ", scenes
 
-        os.mkdir(os.path.join(topdir, camera_id))
+        os.mkdir(os.path.join(topdir, id_combo_string))
         for d in scenes:
-            os.mkdir(os.path.join(topdir, camera_id, d))
+            os.mkdir(os.path.join(topdir, id_combo_string, d))
 
         tot_tests = []
         tot_pass = 0
@@ -382,17 +410,17 @@
             tests.sort()
             tot_tests.extend(tests)
 
-            summary = "Cam" + camera_id + " " + scene + "\n"
+            summary = "Cam" + id_combo_string + " " + scene + "\n"
             numpass = 0
             numskip = 0
             num_not_mandated_fail = 0
             numfail = 0
             validate_switch = True
             if scene_req[scene] is not None:
-                out_path = os.path.join(topdir, camera_id, scene+".jpg")
+                out_path = os.path.join(topdir, id_combo_string, scene+".jpg")
                 out_arg = "out=" + out_path
                 if scene == 'sensor_fusion':
-                    skip_code = skip_sensor_fusion(camera_id)
+                    skip_code = skip_sensor_fusion(id_combo.id)
                     if rot_rig_id or skip_code == SKIP_RET_CODE:
                         validate_switch = False
                 if skip_scene_validation:
@@ -400,7 +428,7 @@
                 cmd = None
                 if auto_scene_switch:
                     if (not merge_result_switch or
-                            (merge_result_switch and camera_ids[0] == '0')):
+                            (merge_result_switch and id_combo_string == '0')):
                         scene_arg = 'scene=' + scene
                         fov_arg = 'fov=' + camera_fov
                         cmd = ['python',
@@ -421,7 +449,7 @@
                 if cmd is not None:
                     valid_scene_code = subprocess.call(cmd, cwd=topdir)
                     assert valid_scene_code == 0
-            print 'Start running ITS on camera %s, %s' % (camera_id, scene)
+            print 'Start running ITS on camera %s, %s' % (id_combo_string, scene)
             # Extract chart from scene for scene3 once up front
             chart_loc_arg = ''
             chart_height = CHART_HEIGHT
@@ -431,14 +459,19 @@
                 chart = its.cv2image.Chart(SCENE3_FILE, chart_height,
                                            chart_distance, CHART_SCALE_START,
                                            CHART_SCALE_STOP, CHART_SCALE_STEP,
-                                           camera_id)
+                                           id_combo.id)
                 chart_loc_arg = 'chart_loc=%.2f,%.2f,%.2f,%.2f,%.3f' % (
                         chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm,
                         chart.scale)
             # Run each test, capturing stdout and stderr.
             for (testname, testpath) in tests:
+                # Only pick predefined tests for hidden physical camera
+                if has_hidden_sub_camera and \
+                        testname not in HIDDEN_PHYSICAL_CAMERA_TESTS[scene]:
+                    numskip += 1
+                    continue
                 if auto_scene_switch:
-                    if merge_result_switch and camera_ids[0] == '0':
+                    if merge_result_switch and id_combo_string == '0':
                         # Send an input event to keep the screen not dimmed.
                         # Since we are not using camera of chart screen, FOCUS event
                         # should do nothing but keep the screen from dimming.
@@ -450,7 +483,7 @@
                         subprocess.call(cmd.split())
                 t0 = time.time()
                 for num_try in range(NUM_TRYS):
-                    outdir = os.path.join(topdir, camera_id, scene)
+                    outdir = os.path.join(topdir, id_combo_string, scene)
                     outpath = os.path.join(outdir, testname+'_stdout.txt')
                     errpath = os.path.join(outdir, testname+'_stderr.txt')
                     if scene == 'sensor_fusion':
@@ -466,7 +499,7 @@
                             test_code = skip_code
                     if skip_code is not SKIP_RET_CODE:
                         cmd = ['python', os.path.join(os.getcwd(), testpath)]
-                        cmd += sys.argv[1:] + [camera_id_arg] + [chart_loc_arg]
+                        cmd += one_camera_argv + ["Camera="+id_combo_string] + [chart_loc_arg]
                         cmd += [chart_dist_arg]
                         with open(outpath, 'w') as fout, open(errpath, 'w') as ferr:
                             test_code = run_subprocess_with_timeout(
@@ -527,7 +560,7 @@
             print "%s compatibility score: %.f/100\n" % (
                     scene, 100.0 * numpass / len(tests))
 
-            summary_path = os.path.join(topdir, camera_id, scene, "summary.txt")
+            summary_path = os.path.join(topdir, id_combo_string, scene, "summary.txt")
             with open(summary_path, "w") as f:
                 f.write(summary)
 
@@ -536,7 +569,10 @@
                                           else ItsSession.RESULT_FAIL)
             results[scene][ItsSession.SUMMARY_KEY] = summary_path
 
-        print "Compatibility Score: %.f/100" % (100.0 * tot_pass / len(tot_tests))
+        if tot_tests:
+            print "Compatibility Score: %.f/100" % (100.0 * tot_pass / len(tot_tests))
+        else:
+            print "Compatibility Score: 0/100"
 
         msg = "Reporting ITS result to CtsVerifier"
         print msg
@@ -544,9 +580,10 @@
         if merge_result_switch:
             # results are modified by report_result
             results_backup = copy.deepcopy(results)
-            its.device.report_result(result_device_id, camera_id, results_backup)
+            its.device.report_result(result_device_id, id_combo_string, results_backup)
 
-        its.device.report_result(device_id, camera_id, results)
+        # Report hidden_physical_id results as well.
+        its.device.report_result(device_id, id_combo_string, results)
 
     if auto_scene_switch:
         if merge_result_switch:
diff --git a/apps/CameraITS/tools/run_sensor_fusion_box.py b/apps/CameraITS/tools/run_sensor_fusion_box.py
index 3c9199a..82f915d 100644
--- a/apps/CameraITS/tools/run_sensor_fusion_box.py
+++ b/apps/CameraITS/tools/run_sensor_fusion_box.py
@@ -91,7 +91,7 @@
     print 'Testing device ' + device_id
 
     # ensure camera_id is valid
-    avail_camera_ids = find_avail_camera_ids(device_id_arg, tmpdir)
+    avail_camera_ids = find_avail_camera_ids()
     if camera_id not in avail_camera_ids:
         print 'Need to specify valid camera_id in ', avail_camera_ids
         sys.exit()
@@ -220,12 +220,9 @@
                 return line
     return None
 
-def find_avail_camera_ids(device_id_arg, tmpdir):
+def find_avail_camera_ids():
     """Find the available camera IDs.
 
-    Args:
-        devices_id_arg(str):    device=###
-        tmpdir(str):            generated tmp dir for run
     Returns:
         list of available cameras
     """
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
index f5306c3..47137b4 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java
@@ -159,6 +159,7 @@
     private CameraCharacteristics mCameraCharacteristics = null;
     private HashMap<String, CameraCharacteristics> mPhysicalCameraChars =
             new HashMap<String, CameraCharacteristics>();
+    private ItsUtils.ItsCameraIdList mItsCameraIdList = null;
 
     private Vibrator mVibrator = null;
 
@@ -362,11 +363,13 @@
         try {
             if (mMemoryQuota == -1) {
                 // Initialize memory quota on this device
-                List<String> devices = ItsUtils.getItsCompatibleCameraIds(mCameraManager);
-                if (devices.size() == 0) {
+                if (mItsCameraIdList == null) {
+                    mItsCameraIdList = ItsUtils.getItsCompatibleCameraIds(mCameraManager);
+                }
+                if (mItsCameraIdList.mCameraIds.size() == 0) {
                     throw new ItsException("No camera devices");
                 }
-                for (String camId : devices) {
+                for (String camId : mItsCameraIdList.mCameraIds) {
                     CameraCharacteristics chars =  mCameraManager.getCameraCharacteristics(camId);
                     Size maxYuvSize = ItsUtils.getMaxOutputSize(
                             chars, ImageFormat.YUV_420_888);
@@ -949,15 +952,17 @@
     }
 
     private void doGetCameraIds() throws ItsException {
-        List<String> devices = ItsUtils.getItsCompatibleCameraIds(mCameraManager);
-        if (devices.size() == 0) {
+        if (mItsCameraIdList == null) {
+            mItsCameraIdList = ItsUtils.getItsCompatibleCameraIds(mCameraManager);
+        }
+        if (mItsCameraIdList.mCameraIdCombos.size() == 0) {
             throw new ItsException("No camera devices");
         }
 
         try {
             JSONObject obj = new JSONObject();
             JSONArray array = new JSONArray();
-            for (String id : devices) {
+            for (String id : mItsCameraIdList.mCameraIdCombos) {
                 array.put(id);
             }
             obj.put("cameraIdArray", array);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsTestActivity.java
index fd62ed2..901fe4c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsTestActivity.java
@@ -91,6 +91,13 @@
             add("scene5");
             add("sensor_fusion");
         } };
+    // This must match scenes of HIDDEN_PHYSICAL_CAMERA_TESTS in run_all_tests.py
+    private static final ArrayList<String> mHiddenPhysicalCameraSceneIds =
+            new ArrayList<String> () { {
+                    add("scene0");
+                    add("scene1");
+                    add("sensor_fusion");
+             }};
 
     // TODO: cache the following in saved bundle
     private Set<ResultKey> mAllScenes = null;
@@ -332,7 +339,8 @@
         // Hide the test if all camera devices are legacy
         CameraManager manager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE);
         try {
-            mToBeTestedCameraIds = ItsUtils.getItsCompatibleCameraIds(manager);
+            ItsUtils.ItsCameraIdList cameraIdList = ItsUtils.getItsCompatibleCameraIds(manager);
+            mToBeTestedCameraIds = cameraIdList.mCameraIdCombos;
         } catch (ItsException e) {
             Toast.makeText(ItsTestActivity.this,
                     "Received error from camera service while checking device capabilities: "
@@ -366,7 +374,8 @@
 
     protected void setupItsTests(ArrayTestListAdapter adapter) {
         for (String cam : mToBeTestedCameraIds) {
-            for (String scene : mSceneIds) {
+            List<String> scenes = cam.contains(":") ? mHiddenPhysicalCameraSceneIds : mSceneIds;
+            for (String scene : scenes) {
                 adapter.add(new DialogTestListItem(this,
                 testTitle(cam, scene),
                 testId(cam, scene)));
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsUtils.java b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsUtils.java
index 6fcaf69..cd739b5 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsUtils.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsUtils.java
@@ -31,17 +31,25 @@
 import android.media.Image.Plane;
 import android.net.Uri;
 import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.util.Log;
 import android.util.Size;
 
+import com.android.ex.camera2.blocking.BlockingCameraManager;
+import com.android.ex.camera2.blocking.BlockingCameraManager.BlockingOpenException;
+import com.android.ex.camera2.blocking.BlockingStateCallback;
+
 import org.json.JSONArray;
 import org.json.JSONObject;
 
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.concurrent.Semaphore;
 import java.util.List;
+import java.util.Set;
 
 
 public class ItsUtils {
@@ -306,13 +314,23 @@
         }
     }
 
-    public static List<String> getItsCompatibleCameraIds(CameraManager manager)
+    public static class ItsCameraIdList {
+        // Short form camera Ids (including both CameraIdList and hidden physical cameras
+        public List<String> mCameraIds;
+        // Camera Id combos (ids from CameraIdList, and hidden physical camera Ids
+        // in the form of [logical camera id]:[hidden physical camera id]
+        public List<String> mCameraIdCombos;
+    }
+
+    public static ItsCameraIdList getItsCompatibleCameraIds(CameraManager manager)
             throws ItsException {
         if (manager == null) {
             throw new IllegalArgumentException("CameraManager is null");
         }
 
-        ArrayList<String> outList = new ArrayList<String>();
+        ItsCameraIdList outList = new ItsCameraIdList();
+        outList.mCameraIds = new ArrayList<String>();
+        outList.mCameraIdCombos = new ArrayList<String>();
         try {
             String[] cameraIds = manager.getCameraIdList();
             for (String id : cameraIds) {
@@ -320,12 +338,17 @@
                 int[] actualCapabilities = characteristics.get(
                         CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
                 boolean haveBC = false;
+                boolean isMultiCamera = false;
                 final int BACKWARD_COMPAT =
                         CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE;
+                final int LOGICAL_MULTI_CAMERA =
+                        CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA;
                 for (int capability : actualCapabilities) {
                     if (capability == BACKWARD_COMPAT) {
                         haveBC = true;
-                        break;
+                    }
+                    if (capability == LOGICAL_MULTI_CAMERA) {
+                        isMultiCamera = true;
                     }
                 }
 
@@ -339,7 +362,39 @@
                     // Skip LEGACY and EXTERNAL devices
                     continue;
                 }
-                outList.add(id);
+                outList.mCameraIds.add(id);
+                outList.mCameraIdCombos.add(id);
+
+                // Only add hidden physical cameras for multi-camera.
+                if (!isMultiCamera) continue;
+
+                float defaultFocalLength = getLogicalCameraDefaultFocalLength(manager, id);
+                Set<String> physicalIds = characteristics.getPhysicalCameraIds();
+                for (String physicalId : physicalIds) {
+                    if (Arrays.asList(cameraIds).contains(physicalId)) continue;
+
+                    CameraCharacteristics physicalChar =
+                            manager.getCameraCharacteristics(physicalId);
+                    hwLevel = characteristics.get(
+                            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
+                    if (hwLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY ||
+                            hwLevel ==
+                            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL) {
+                        // Skip LEGACY and EXTERNAL devices
+                        continue;
+                    }
+
+                    // To reduce duplicate tests, only additionally test hidden physical cameras
+                    // with different focal length compared to the default focal length of the
+                    // logical camera.
+                    float[] physicalFocalLengths = physicalChar.get(
+                            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
+                    if (defaultFocalLength != physicalFocalLengths[0]) {
+                        outList.mCameraIds.add(physicalId);
+                        outList.mCameraIdCombos.add(id + ":" + physicalId);
+                    }
+                }
+
             }
         } catch (CameraAccessException e) {
             Logt.e(TAG,
@@ -348,4 +403,32 @@
         }
         return outList;
     }
+
+    public static float getLogicalCameraDefaultFocalLength(CameraManager manager,
+            String cameraId) throws ItsException {
+        BlockingCameraManager blockingManager = new BlockingCameraManager(manager);
+        BlockingStateCallback listener = new BlockingStateCallback();
+        HandlerThread cameraThread = new HandlerThread("ItsUtilThread");
+        cameraThread.start();
+        Handler cameraHandler = new Handler(cameraThread.getLooper());
+        CameraDevice camera = null;
+        float defaultFocalLength = 0.0f;
+
+        try {
+            camera = blockingManager.openCamera(cameraId, listener, cameraHandler);
+            CaptureRequest.Builder previewBuilder =
+                    camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+            defaultFocalLength = previewBuilder.get(CaptureRequest.LENS_FOCAL_LENGTH);
+        } catch (Exception e) {
+            throw new ItsException("Failed to query default focal length for logical camera", e);
+        } finally {
+            if (camera != null) {
+                camera.close();
+            }
+            if (cameraThread != null) {
+                cameraThread.quitSafely();
+            }
+        }
+        return defaultFocalLength;
+    }
 }