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