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