/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.deviceaswebcam; import android.content.Context; import android.util.ArrayMap; import android.util.JsonReader; import android.util.Log; import android.util.Range; import androidx.annotation.Nullable; import androidx.core.util.Preconditions; import com.android.DeviceAsWebcam.R; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Objects; /** * A class for providing camera related information overridden by vendors through resource overlays. */ public class VendorCameraPrefs { private static final String TAG = "VendorCameraPrefs"; public static class PhysicalCameraInfo { public final String physicalCameraId; // Camera category which might help UI labelling while cycling through camera ids. public final CameraCategory cameraCategory; @Nullable public final Range zoomRatioRange; PhysicalCameraInfo(String physicalCameraIdI, CameraCategory cameraCategoryI, @Nullable Range zoomRatioRangeI) { physicalCameraId = physicalCameraIdI; cameraCategory = cameraCategoryI; zoomRatioRange = zoomRatioRangeI; } } public VendorCameraPrefs(ArrayMap> logicalToPhysicalMap, List ignoredCameraList) { mLogicalToPhysicalMap = logicalToPhysicalMap; mIgnoredCameraList = ignoredCameraList; } @Nullable public List getPhysicalCameraInfos(String cameraId) { return mLogicalToPhysicalMap.get(cameraId); } /** * Returns the custom physical camera zoom ratio range. Returns {@code null} if no custom value * can be found. * *

This is used to specify the available zoom ratio range when the working camera is a * physical camera under a logical camera. */ @Nullable public Range getPhysicalCameraZoomRatioRange(CameraId cameraId) { PhysicalCameraInfo physicalCameraInfo = getPhysicalCameraInfo(cameraId); return physicalCameraInfo != null ? physicalCameraInfo.zoomRatioRange : null; } /** * Retrieves the {@link CameraCategory} if it is specified by the vendor camera prefs data. */ public CameraCategory getCameraCategory(CameraId cameraId) { PhysicalCameraInfo physicalCameraInfo = getPhysicalCameraInfo(cameraId); return physicalCameraInfo != null ? physicalCameraInfo.cameraCategory : CameraCategory.UNKNOWN; } /** * Returns the {@link PhysicalCameraInfo} corresponding to the specified camera id. Returns * null if no item can be found. */ private PhysicalCameraInfo getPhysicalCameraInfo(CameraId cameraId) { List physicalCameraInfos = getPhysicalCameraInfos( cameraId.mainCameraId); if (physicalCameraInfos != null) { for (PhysicalCameraInfo physicalCameraInfo : physicalCameraInfos) { if (Objects.equals(physicalCameraInfo.physicalCameraId, cameraId.physicalCameraId)) { return physicalCameraInfo; } } } return null; } /** * Returns the ignored camera list. */ public List getIgnoredCameraList() { return mIgnoredCameraList; } // logical camera -> PhysicalCameraInfo. The list of PhysicalCameraInfos // is in order of preference for the physical streams that must be used by // DeviceAsWebcam service. private final ArrayMap> mLogicalToPhysicalMap; // The ignored camera list. private final List mIgnoredCameraList; /** * Converts an InputStream into a String * * @param in InputStream * @return InputStream converted to a String */ private static String inputStreamToString(InputStream in) throws IOException { StringBuilder builder = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { reader.lines().forEach(builder::append); } return builder.toString(); } /** * Returns an instance of {@link VendorCameraPrefs} that does not provide Physical * Camera Mapping. Used for when we want to force CameraController to use the logical * cameras. The returned VendorCameraPrefs still honors ignored cameras retrieved from * {@link #getIgnoredCameralist}. */ public static VendorCameraPrefs createEmptyVendorCameraPrefs(Context context) { List ignoredCameraList = getIgnoredCameralist(context); return new VendorCameraPrefs(new ArrayMap<>(), ignoredCameraList); } /** * Reads the vendor camera preferences from the custom JSON files. * * @param context Application context which can be used to retrieve resources. */ public static VendorCameraPrefs getVendorCameraPrefsFromJson(Context context) { ArrayMap> zoomRatioRangeInfo = getZoomRatioRangeInfo(context); ArrayMap> logicalToPhysicalMap = createLogicalToPhysicalMap(context, zoomRatioRangeInfo); List ignoredCameraList = getIgnoredCameralist(context); return new VendorCameraPrefs(logicalToPhysicalMap, ignoredCameraList); } /** * Creates a logical to physical camera map by parsing the physical camera mapping info from * the input which is expected to be a valid JSON stream. * * @param context Application context which can be used to retrieve resources. * @param zoomRatioRangeInfo A map contains the physical camera zoom ratio range info. This is * used to created the PhysicalCameraInfo. */ private static ArrayMap> createLogicalToPhysicalMap( Context context, ArrayMap> zoomRatioRangeInfo) { InputStream in = context.getResources().openRawResource(R.raw.physical_camera_mapping); ArrayMap> logicalToPhysicalMap = new ArrayMap<>(); try { JSONObject physicalCameraMapping = new JSONObject(inputStreamToString(in)); for (String logCam : physicalCameraMapping.keySet()) { JSONObject physicalCameraObj = physicalCameraMapping.getJSONObject(logCam); List physicalCameraIds = new ArrayList<>(); for (String physCam : physicalCameraObj.keySet()) { String identifier = CameraId.createIdentifier(logCam, physCam); physicalCameraIds.add(new PhysicalCameraInfo(physCam, convertLabelToCameraCategory(physicalCameraObj.getString(physCam)), zoomRatioRangeInfo.get(identifier))); } logicalToPhysicalMap.put(logCam, physicalCameraIds); } } catch (JSONException | IOException e) { Log.e(TAG, "Failed to parse JSON", e); } return logicalToPhysicalMap; } /** * Converts the label string to corresponding {@link CameraCategory}. */ private static CameraCategory convertLabelToCameraCategory(String label) { return switch (label) { case "W" -> CameraCategory.WIDE_ANGLE; case "UW" -> CameraCategory.ULTRA_WIDE; case "T" -> CameraCategory.TELEPHOTO; case "S" -> CameraCategory.STANDARD; case "O" -> CameraCategory.OTHER; default -> CameraCategory.UNKNOWN; }; } /** * Obtains the zoom ratio range info from the input which is expected to be a valid * JSON stream. * * @param context Application context which can be used to retrieve resources. */ private static ArrayMap> getZoomRatioRangeInfo(Context context) { InputStream in = context.getResources().openRawResource( R.raw.physical_camera_zoom_ratio_ranges); ArrayMap> zoomRatioRangeInfo = new ArrayMap<>(); try { JSONObject physicalCameraMapping = new JSONObject(inputStreamToString(in)); for (String logCam : physicalCameraMapping.keySet()) { JSONObject physicalCameraObj = physicalCameraMapping.getJSONObject(logCam); for (String physCam : physicalCameraObj.keySet()) { String identifier = CameraId.createIdentifier(logCam, physCam); JSONArray zoomRatioRangeArray = physicalCameraObj.getJSONArray(physCam); Preconditions.checkArgument(zoomRatioRangeArray.length() == 2, "Incorrect number of values in zoom ratio range. Expected: %d, Found:" + " %d", 2, zoomRatioRangeArray.length()); boolean isAvailable = zoomRatioRangeArray.getDouble(0) > 0.0 && zoomRatioRangeArray.getDouble(1) > 0.0 && zoomRatioRangeArray.getDouble(0) < zoomRatioRangeArray.getDouble(1); Preconditions.checkArgument(isAvailable, "Incorrect zoom ratio range values. All values should be > 0.0 and " + "the first value should be lower than the second value."); zoomRatioRangeInfo.put(identifier, Range.create((float) zoomRatioRangeArray.getDouble(0), (float)zoomRatioRangeArray.getDouble(1))); } } } catch (JSONException | IOException e) { Log.e(TAG, "Failed to parse JSON", e); } return zoomRatioRangeInfo; } /** * Retrieves the ignored camera list from the input which is expected to be a valid JSON stream. */ private static List getIgnoredCameralist(Context context) { List ignoredCameras = new ArrayList<>(); try(InputStream in = context.getResources().openRawResource(R.raw.ignored_cameras); JsonReader jsonReader = new JsonReader(new InputStreamReader(in))) { jsonReader.beginArray(); while (jsonReader.hasNext()) { String node = jsonReader.nextString(); ignoredCameras.add(node); } jsonReader.endArray(); } catch (IOException e) { Log.e(TAG, "Failed to parse JSON. Running with a partial ignored camera list", e); } return ignoredCameras; } }