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 * Reads the vendor camera preferences from the custom JSON files. 146 * 147 * @param context Application context which can be used to retrieve resources. 148 */ getVendorCameraPrefsFromJson(Context context)149 public static VendorCameraPrefs getVendorCameraPrefsFromJson(Context context) { 150 ArrayMap<String, Range<Float>> zoomRatioRangeInfo = getZoomRatioRangeInfo(context); 151 ArrayMap<String, List<PhysicalCameraInfo>> logicalToPhysicalMap = 152 createLogicalToPhysicalMap(context, zoomRatioRangeInfo); 153 List<String> ignoredCameraList = getIgnoredCameralist(context); 154 return new VendorCameraPrefs(logicalToPhysicalMap, ignoredCameraList); 155 } 156 157 /** 158 * Creates a logical to physical camera map by parsing the physical camera mapping info from 159 * the input which is expected to be a valid JSON stream. 160 * 161 * @param context Application context which can be used to retrieve resources. 162 * @param zoomRatioRangeInfo A map contains the physical camera zoom ratio range info. This is 163 * used to created the PhysicalCameraInfo. 164 */ createLogicalToPhysicalMap( Context context, ArrayMap<String, Range<Float>> zoomRatioRangeInfo)165 private static ArrayMap<String, List<PhysicalCameraInfo>> createLogicalToPhysicalMap( 166 Context context, ArrayMap<String, Range<Float>> zoomRatioRangeInfo) { 167 InputStream in = context.getResources().openRawResource(R.raw.physical_camera_mapping); 168 ArrayMap<String, List<PhysicalCameraInfo>> logicalToPhysicalMap = new ArrayMap<>(); 169 try { 170 JSONObject physicalCameraMapping = new JSONObject(inputStreamToString(in)); 171 for (String logCam : physicalCameraMapping.keySet()) { 172 JSONObject physicalCameraObj = physicalCameraMapping.getJSONObject(logCam); 173 List<PhysicalCameraInfo> physicalCameraIds = new ArrayList<>(); 174 for (String physCam : physicalCameraObj.keySet()) { 175 String identifier = CameraId.createIdentifier(logCam, physCam); 176 physicalCameraIds.add(new PhysicalCameraInfo(physCam, 177 convertLabelToCameraCategory(physicalCameraObj.getString(physCam)), 178 zoomRatioRangeInfo.get(identifier))); 179 } 180 logicalToPhysicalMap.put(logCam, physicalCameraIds); 181 } 182 } catch (JSONException | IOException e) { 183 Log.e(TAG, "Failed to parse JSON", e); 184 } 185 return logicalToPhysicalMap; 186 } 187 188 /** 189 * Converts the label string to corresponding {@link CameraCategory}. 190 */ convertLabelToCameraCategory(String label)191 private static CameraCategory convertLabelToCameraCategory(String label) { 192 return switch (label) { 193 case "W" -> CameraCategory.WIDE_ANGLE; 194 case "UW" -> CameraCategory.ULTRA_WIDE; 195 case "T" -> CameraCategory.TELEPHOTO; 196 case "S" -> CameraCategory.STANDARD; 197 case "O" -> CameraCategory.OTHER; 198 default -> CameraCategory.UNKNOWN; 199 }; 200 } 201 202 /** 203 * Obtains the zoom ratio range info from the input which is expected to be a valid 204 * JSON stream. 205 * 206 * @param context Application context which can be used to retrieve resources. 207 */ getZoomRatioRangeInfo(Context context)208 private static ArrayMap<String, Range<Float>> getZoomRatioRangeInfo(Context context) { 209 InputStream in = context.getResources().openRawResource( 210 R.raw.physical_camera_zoom_ratio_ranges); 211 ArrayMap<String, Range<Float>> zoomRatioRangeInfo = new ArrayMap<>(); 212 try { 213 JSONObject physicalCameraMapping = new JSONObject(inputStreamToString(in)); 214 for (String logCam : physicalCameraMapping.keySet()) { 215 JSONObject physicalCameraObj = physicalCameraMapping.getJSONObject(logCam); 216 for (String physCam : physicalCameraObj.keySet()) { 217 String identifier = CameraId.createIdentifier(logCam, physCam); 218 JSONArray zoomRatioRangeArray = physicalCameraObj.getJSONArray(physCam); 219 Preconditions.checkArgument(zoomRatioRangeArray.length() == 2, 220 "Incorrect number of values in zoom ratio range. Expected: %d, Found:" 221 + " %d", 2, zoomRatioRangeArray.length()); 222 boolean isAvailable = zoomRatioRangeArray.getDouble(0) > 0.0 223 && zoomRatioRangeArray.getDouble(1) > 0.0 224 && zoomRatioRangeArray.getDouble(0) < zoomRatioRangeArray.getDouble(1); 225 Preconditions.checkArgument(isAvailable, 226 "Incorrect zoom ratio range values. All values should be > 0.0 and " 227 + "the first value should be lower than the second value."); 228 zoomRatioRangeInfo.put(identifier, 229 Range.create((float) zoomRatioRangeArray.getDouble(0), 230 (float)zoomRatioRangeArray.getDouble(1))); 231 } 232 } 233 } catch (JSONException | IOException e) { 234 Log.e(TAG, "Failed to parse JSON", e); 235 } 236 return zoomRatioRangeInfo; 237 } 238 239 /** 240 * Retrieves the ignored camera list from the input which is expected to be a valid JSON stream. 241 */ 242 private static List<String> getIgnoredCameralist(Context context) { 243 List<String> ignoredCameras = new ArrayList<>(); 244 try(InputStream in = context.getResources().openRawResource(R.raw.ignored_cameras); 245 JsonReader jsonReader = new JsonReader(new InputStreamReader(in))) { 246 jsonReader.beginArray(); 247 while (jsonReader.hasNext()) { 248 String node = jsonReader.nextString(); 249 ignoredCameras.add(node); 250 } 251 jsonReader.endArray(); 252 } catch (IOException e) { 253 Log.e(TAG, "Failed to parse JSON. Running with a partial ignored camera list", e); 254 } 255 256 return ignoredCameras; 257 } 258 } 259