1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.DeviceAsWebcam; 18 19 import android.content.Context; 20 import android.util.ArrayMap; 21 import android.util.JsonReader; 22 import android.util.Log; 23 import android.util.Range; 24 25 import androidx.annotation.Nullable; 26 import androidx.core.util.Preconditions; 27 28 import org.json.JSONArray; 29 import org.json.JSONException; 30 import org.json.JSONObject; 31 32 import java.io.BufferedReader; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.InputStreamReader; 36 import java.nio.charset.StandardCharsets; 37 import java.util.ArrayList; 38 import java.util.List; 39 import java.util.Objects; 40 41 /** 42 * A class for providing camera related information overridden by vendors through resource overlays. 43 */ 44 public class VendorCameraPrefs { 45 private static final String TAG = "VendorCameraPrefs"; 46 47 public static class PhysicalCameraInfo { 48 public final String physicalCameraId; 49 // Camera category which might help UI labelling while cycling through camera ids. 50 public final CameraCategory cameraCategory; 51 @Nullable 52 public final Range<Float> zoomRatioRange; 53 PhysicalCameraInfo(String physicalCameraIdI, CameraCategory cameraCategoryI, @Nullable Range<Float> zoomRatioRangeI)54 PhysicalCameraInfo(String physicalCameraIdI, CameraCategory cameraCategoryI, 55 @Nullable Range<Float> zoomRatioRangeI) { 56 physicalCameraId = physicalCameraIdI; 57 cameraCategory = cameraCategoryI; 58 zoomRatioRange = zoomRatioRangeI; 59 } 60 } 61 VendorCameraPrefs(ArrayMap<String, List<PhysicalCameraInfo>> logicalToPhysicalMap, List<String> ignoredCameraList)62 public VendorCameraPrefs(ArrayMap<String, List<PhysicalCameraInfo>> logicalToPhysicalMap, 63 List<String> ignoredCameraList) { 64 mLogicalToPhysicalMap = logicalToPhysicalMap; 65 mIgnoredCameraList = ignoredCameraList; 66 } 67 68 @Nullable getPhysicalCameraInfos(String cameraId)69 public List<PhysicalCameraInfo> getPhysicalCameraInfos(String cameraId) { 70 return mLogicalToPhysicalMap.get(cameraId); 71 } 72 73 /** 74 * Returns the custom physical camera zoom ratio range. Returns {@code null} if no custom value 75 * can be found. 76 * 77 * <p>This is used to specify the available zoom ratio range when the working camera is a 78 * physical camera under a logical camera. 79 */ 80 @Nullable getPhysicalCameraZoomRatioRange(CameraId cameraId)81 public Range<Float> getPhysicalCameraZoomRatioRange(CameraId cameraId) { 82 PhysicalCameraInfo physicalCameraInfo = getPhysicalCameraInfo(cameraId); 83 return physicalCameraInfo != null ? physicalCameraInfo.zoomRatioRange : null; 84 } 85 86 /** 87 * Retrieves the {@link CameraCategory} if it is specified by the vendor camera prefs data. 88 */ getCameraCategory(CameraId cameraId)89 public CameraCategory getCameraCategory(CameraId cameraId) { 90 PhysicalCameraInfo physicalCameraInfo = getPhysicalCameraInfo(cameraId); 91 return physicalCameraInfo != null ? physicalCameraInfo.cameraCategory 92 : CameraCategory.UNKNOWN; 93 } 94 95 /** 96 * Returns the {@link PhysicalCameraInfo} corresponding to the specified camera id. Returns 97 * null if no item can be found. 98 */ getPhysicalCameraInfo(CameraId cameraId)99 private PhysicalCameraInfo getPhysicalCameraInfo(CameraId cameraId) { 100 List<PhysicalCameraInfo> physicalCameraInfos = getPhysicalCameraInfos( 101 cameraId.mainCameraId); 102 103 if (physicalCameraInfos != null) { 104 for (PhysicalCameraInfo physicalCameraInfo : physicalCameraInfos) { 105 if (Objects.equals(physicalCameraInfo.physicalCameraId, 106 cameraId.physicalCameraId)) { 107 return physicalCameraInfo; 108 } 109 } 110 } 111 112 return null; 113 } 114 115 /** 116 * Returns the ignored camera list. 117 */ getIgnoredCameraList()118 public List<String> getIgnoredCameraList() { 119 return mIgnoredCameraList; 120 } 121 122 // logical camera -> PhysicalCameraInfo. The list of PhysicalCameraInfos 123 // is in order of preference for the physical streams that must be used by 124 // DeviceAsWebcam service. 125 private final ArrayMap<String, List<PhysicalCameraInfo>> mLogicalToPhysicalMap; 126 // The ignored camera list. 127 private final List<String> mIgnoredCameraList; 128 129 /** 130 * Converts an InputStream into a String 131 * 132 * @param in InputStream 133 * @return InputStream converted to a String 134 */ inputStreamToString(InputStream in)135 private static String inputStreamToString(InputStream in) throws IOException { 136 StringBuilder builder = new StringBuilder(); 137 try (BufferedReader reader = 138 new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { 139 reader.lines().forEach(builder::append); 140 } 141 return builder.toString(); 142 } 143 144 /** 145 * Returns an instance of {@link VendorCameraPrefs} that does not provide Physical 146 * Camera Mapping. Used for when we want to force CameraController to use the logical 147 * cameras. The returned VendorCameraPrefs still honors ignored cameras retrieved from 148 * {@link #getIgnoredCameralist}. 149 */ createEmptyVendorCameraPrefs(Context context)150 public static VendorCameraPrefs createEmptyVendorCameraPrefs(Context context) { 151 List<String> ignoredCameraList = getIgnoredCameralist(context); 152 return new VendorCameraPrefs(new ArrayMap<>(), ignoredCameraList); 153 } 154 155 /** 156 * Reads the vendor camera preferences from the custom JSON files. 157 * 158 * @param context Application context which can be used to retrieve resources. 159 */ getVendorCameraPrefsFromJson(Context context)160 public static VendorCameraPrefs getVendorCameraPrefsFromJson(Context context) { 161 ArrayMap<String, Range<Float>> zoomRatioRangeInfo = getZoomRatioRangeInfo(context); 162 ArrayMap<String, List<PhysicalCameraInfo>> logicalToPhysicalMap = 163 createLogicalToPhysicalMap(context, zoomRatioRangeInfo); 164 List<String> ignoredCameraList = getIgnoredCameralist(context); 165 return new VendorCameraPrefs(logicalToPhysicalMap, ignoredCameraList); 166 } 167 168 /** 169 * Creates a logical to physical camera map by parsing the physical camera mapping info from 170 * the input which is expected to be a valid JSON stream. 171 * 172 * @param context Application context which can be used to retrieve resources. 173 * @param zoomRatioRangeInfo A map contains the physical camera zoom ratio range info. This is 174 * used to created the PhysicalCameraInfo. 175 */ createLogicalToPhysicalMap( Context context, ArrayMap<String, Range<Float>> zoomRatioRangeInfo)176 private static ArrayMap<String, List<PhysicalCameraInfo>> createLogicalToPhysicalMap( 177 Context context, ArrayMap<String, Range<Float>> zoomRatioRangeInfo) { 178 InputStream in = context.getResources().openRawResource(R.raw.physical_camera_mapping); 179 ArrayMap<String, List<PhysicalCameraInfo>> logicalToPhysicalMap = new ArrayMap<>(); 180 try { 181 JSONObject physicalCameraMapping = new JSONObject(inputStreamToString(in)); 182 for (String logCam : physicalCameraMapping.keySet()) { 183 JSONObject physicalCameraObj = physicalCameraMapping.getJSONObject(logCam); 184 List<PhysicalCameraInfo> physicalCameraIds = new ArrayList<>(); 185 for (String physCam : physicalCameraObj.keySet()) { 186 String identifier = CameraId.createIdentifier(logCam, physCam); 187 physicalCameraIds.add(new PhysicalCameraInfo(physCam, 188 convertLabelToCameraCategory(physicalCameraObj.getString(physCam)), 189 zoomRatioRangeInfo.get(identifier))); 190 } 191 logicalToPhysicalMap.put(logCam, physicalCameraIds); 192 } 193 } catch (JSONException | IOException e) { 194 Log.e(TAG, "Failed to parse JSON", e); 195 } 196 return logicalToPhysicalMap; 197 } 198 199 /** 200 * Converts the label string to corresponding {@link CameraCategory}. 201 */ convertLabelToCameraCategory(String label)202 private static CameraCategory convertLabelToCameraCategory(String label) { 203 return switch (label) { 204 case "W" -> CameraCategory.WIDE_ANGLE; 205 case "UW" -> CameraCategory.ULTRA_WIDE; 206 case "T" -> CameraCategory.TELEPHOTO; 207 case "S" -> CameraCategory.STANDARD; 208 case "O" -> CameraCategory.OTHER; 209 default -> CameraCategory.UNKNOWN; 210 }; 211 } 212 213 /** 214 * Obtains the zoom ratio range info from the input which is expected to be a valid 215 * JSON stream. 216 * 217 * @param context Application context which can be used to retrieve resources. 218 */ getZoomRatioRangeInfo(Context context)219 private static ArrayMap<String, Range<Float>> getZoomRatioRangeInfo(Context context) { 220 InputStream in = context.getResources().openRawResource( 221 R.raw.physical_camera_zoom_ratio_ranges); 222 ArrayMap<String, Range<Float>> zoomRatioRangeInfo = new ArrayMap<>(); 223 try { 224 JSONObject physicalCameraMapping = new JSONObject(inputStreamToString(in)); 225 for (String logCam : physicalCameraMapping.keySet()) { 226 JSONObject physicalCameraObj = physicalCameraMapping.getJSONObject(logCam); 227 for (String physCam : physicalCameraObj.keySet()) { 228 String identifier = CameraId.createIdentifier(logCam, physCam); 229 JSONArray zoomRatioRangeArray = physicalCameraObj.getJSONArray(physCam); 230 Preconditions.checkArgument(zoomRatioRangeArray.length() == 2, 231 "Incorrect number of values in zoom ratio range. Expected: %d, Found:" 232 + " %d", 2, zoomRatioRangeArray.length()); 233 boolean isAvailable = zoomRatioRangeArray.getDouble(0) > 0.0 234 && zoomRatioRangeArray.getDouble(1) > 0.0 235 && zoomRatioRangeArray.getDouble(0) < zoomRatioRangeArray.getDouble(1); 236 Preconditions.checkArgument(isAvailable, 237 "Incorrect zoom ratio range values. All values should be > 0.0 and " 238 + "the first value should be lower than the second value."); 239 zoomRatioRangeInfo.put(identifier, 240 Range.create((float) zoomRatioRangeArray.getDouble(0), 241 (float)zoomRatioRangeArray.getDouble(1))); 242 } 243 } 244 } catch (JSONException | IOException e) { 245 Log.e(TAG, "Failed to parse JSON", e); 246 } 247 return zoomRatioRangeInfo; 248 } 249 250 /** 251 * Retrieves the ignored camera list from the input which is expected to be a valid JSON stream. 252 */ 253 private static List<String> getIgnoredCameralist(Context context) { 254 List<String> ignoredCameras = new ArrayList<>(); 255 try(InputStream in = context.getResources().openRawResource(R.raw.ignored_cameras); 256 JsonReader jsonReader = new JsonReader(new InputStreamReader(in))) { 257 jsonReader.beginArray(); 258 while (jsonReader.hasNext()) { 259 String node = jsonReader.nextString(); 260 ignoredCameras.add(node); 261 } 262 jsonReader.endArray(); 263 } catch (IOException e) { 264 Log.e(TAG, "Failed to parse JSON. Running with a partial ignored camera list", e); 265 } 266 267 return ignoredCameras; 268 } 269 } 270