1 /*
2  * Copyright (C) 2022 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.server.companion.virtual;
18 
19 import static android.hardware.camera2.CameraInjectionSession.InjectionStatusCallback.ERROR_INJECTION_UNSUPPORTED;
20 
21 import android.annotation.NonNull;
22 import android.annotation.UserIdInt;
23 import android.content.Context;
24 import android.content.pm.ApplicationInfo;
25 import android.content.pm.PackageManager;
26 import android.content.pm.UserInfo;
27 import android.hardware.camera2.CameraAccessException;
28 import android.hardware.camera2.CameraInjectionSession;
29 import android.hardware.camera2.CameraManager;
30 import android.os.Process;
31 import android.os.UserManager;
32 import android.util.ArrayMap;
33 import android.util.ArraySet;
34 import android.util.Slog;
35 
36 import com.android.internal.annotations.GuardedBy;
37 
38 import java.util.List;
39 import java.util.Set;
40 
41 /**
42  * Handles blocking access to the camera for apps running on virtual devices.
43  */
44 class CameraAccessController extends CameraManager.AvailabilityCallback implements AutoCloseable {
45     private static final String TAG = "CameraAccessController";
46 
47     private final Object mLock = new Object();
48     private final Object mObserverLock = new Object();
49 
50     private final Context mContext;
51     private final VirtualDeviceManagerInternal mVirtualDeviceManagerInternal;
52     private final CameraAccessBlockedCallback mBlockedCallback;
53     private final CameraManager mCameraManager;
54     private final PackageManager mPackageManager;
55     private final UserManager mUserManager;
56 
57     @GuardedBy("mObserverLock")
58     private int mObserverCount = 0;
59 
60     @GuardedBy("mLock")
61     private ArrayMap<String, InjectionSessionData> mPackageToSessionData = new ArrayMap<>();
62 
63     /**
64      * Mapping from camera ID to open camera app associations. Key is the camera id, value is the
65      * information of the app's uid and package name.
66      */
67     @GuardedBy("mLock")
68     private ArrayMap<String, OpenCameraInfo> mAppsToBlockOnVirtualDevice = new ArrayMap<>();
69 
70     static class InjectionSessionData {
71         public int appUid;
72         public ArrayMap<String, CameraInjectionSession> cameraIdToSession = new ArrayMap<>();
73     }
74 
75     static class OpenCameraInfo {
76         public String packageName;
77         public Set<Integer> packageUids;
78     }
79 
80     interface CameraAccessBlockedCallback {
81         /**
82          * Called whenever an app was blocked from accessing a camera.
83          * @param appUid uid for the app which was blocked
84          */
onCameraAccessBlocked(int appUid)85         void onCameraAccessBlocked(int appUid);
86     }
87 
CameraAccessController(Context context, VirtualDeviceManagerInternal virtualDeviceManagerInternal, CameraAccessBlockedCallback blockedCallback)88     CameraAccessController(Context context,
89             VirtualDeviceManagerInternal virtualDeviceManagerInternal,
90             CameraAccessBlockedCallback blockedCallback) {
91         mContext = context;
92         mVirtualDeviceManagerInternal = virtualDeviceManagerInternal;
93         mBlockedCallback = blockedCallback;
94         mCameraManager = mContext.getSystemService(CameraManager.class);
95         mPackageManager = mContext.getPackageManager();
96         mUserManager = mContext.getSystemService(UserManager.class);
97     }
98 
99     /**
100      * Returns the userId for which the camera access should be blocked.
101      */
102     @UserIdInt
getUserId()103     public int getUserId() {
104         return mContext.getUserId();
105     }
106 
107     /**
108      * Returns the number of observers currently relying on this controller.
109      */
getObserverCount()110     public int getObserverCount() {
111         synchronized (mObserverLock) {
112             return mObserverCount;
113         }
114     }
115 
116     /**
117      * Starts watching for camera access by uids running on a virtual device, if we were not
118      * already doing so.
119      */
startObservingIfNeeded()120     public void startObservingIfNeeded() {
121         synchronized (mObserverLock) {
122             if (mObserverCount == 0) {
123                 mCameraManager.registerAvailabilityCallback(mContext.getMainExecutor(), this);
124             }
125             mObserverCount++;
126         }
127     }
128 
129     /**
130      * Stop watching for camera access.
131      */
stopObservingIfNeeded()132     public void stopObservingIfNeeded() {
133         synchronized (mObserverLock) {
134             mObserverCount--;
135             if (mObserverCount <= 0) {
136                 close();
137             }
138         }
139     }
140 
141     /**
142      * Need to block camera access for applications running on virtual displays.
143      * <p>
144      * Apps that open the camera on the main display will need to block camera access if moved to a
145      * virtual display.
146      *
147      * @param runningUids uids of the application running on the virtual display
148      */
blockCameraAccessIfNeeded(Set<Integer> runningUids)149     public void blockCameraAccessIfNeeded(Set<Integer> runningUids) {
150         synchronized (mLock) {
151             for (int i = 0; i < mAppsToBlockOnVirtualDevice.size(); i++) {
152                 final String cameraId = mAppsToBlockOnVirtualDevice.keyAt(i);
153                 final OpenCameraInfo openCameraInfo = mAppsToBlockOnVirtualDevice.get(cameraId);
154                 final String packageName = openCameraInfo.packageName;
155                 for (int packageUid : openCameraInfo.packageUids) {
156                     if (runningUids.contains(packageUid)) {
157                         InjectionSessionData data = mPackageToSessionData.get(packageName);
158                         if (data == null) {
159                             data = new InjectionSessionData();
160                             data.appUid = packageUid;
161                             mPackageToSessionData.put(packageName, data);
162                         }
163                         startBlocking(packageName, cameraId);
164                         break;
165                     }
166                 }
167             }
168         }
169     }
170 
171     @Override
close()172     public void close() {
173         synchronized (mObserverLock) {
174             if (mObserverCount < 0) {
175                 Slog.wtf(TAG, "Unexpected negative mObserverCount: " + mObserverCount);
176             } else if (mObserverCount > 0) {
177                 Slog.w(TAG, "Unexpected close with observers remaining: " + mObserverCount);
178             }
179         }
180         mCameraManager.unregisterAvailabilityCallback(this);
181     }
182 
183     @Override
onCameraOpened(@onNull String cameraId, @NonNull String packageName)184     public void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) {
185         synchronized (mLock) {
186             InjectionSessionData data = mPackageToSessionData.get(packageName);
187             List<UserInfo> aliveUsers = mUserManager.getAliveUsers();
188             ArraySet<Integer> packageUids = new ArraySet<>();
189             for (UserInfo user : aliveUsers) {
190                 int userId = user.getUserHandle().getIdentifier();
191                 int appUid = queryUidFromPackageName(userId, packageName);
192                 if (mVirtualDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(appUid)) {
193                     if (data == null) {
194                         data = new InjectionSessionData();
195                         data.appUid = appUid;
196                         mPackageToSessionData.put(packageName, data);
197                     }
198                     if (data.cameraIdToSession.containsKey(cameraId)) {
199                         return;
200                     }
201                     startBlocking(packageName, cameraId);
202                     return;
203                 } else {
204                     if (appUid != Process.INVALID_UID) {
205                         packageUids.add(appUid);
206                     }
207                 }
208             }
209             OpenCameraInfo openCameraInfo = new OpenCameraInfo();
210             openCameraInfo.packageName = packageName;
211             openCameraInfo.packageUids = packageUids;
212             mAppsToBlockOnVirtualDevice.put(cameraId, openCameraInfo);
213             CameraInjectionSession existingSession =
214                     (data != null) ? data.cameraIdToSession.get(cameraId) : null;
215             if (existingSession != null) {
216                 existingSession.close();
217                 data.cameraIdToSession.remove(cameraId);
218                 if (data.cameraIdToSession.isEmpty()) {
219                     mPackageToSessionData.remove(packageName);
220                 }
221             }
222         }
223     }
224 
225     @Override
onCameraClosed(@onNull String cameraId)226     public void onCameraClosed(@NonNull String cameraId) {
227         synchronized (mLock) {
228             mAppsToBlockOnVirtualDevice.remove(cameraId);
229             for (int i = mPackageToSessionData.size() - 1; i >= 0; i--) {
230                 InjectionSessionData data = mPackageToSessionData.valueAt(i);
231                 CameraInjectionSession session = data.cameraIdToSession.get(cameraId);
232                 if (session != null) {
233                     session.close();
234                     data.cameraIdToSession.remove(cameraId);
235                     if (data.cameraIdToSession.isEmpty()) {
236                         mPackageToSessionData.removeAt(i);
237                     }
238                 }
239             }
240         }
241     }
242 
243     /**
244      * Turns on blocking for a particular camera and package.
245      */
startBlocking(String packageName, String cameraId)246     private void startBlocking(String packageName, String cameraId) {
247         try {
248             Slog.d(
249                     TAG,
250                     "startBlocking() cameraId: " + cameraId + " packageName: " + packageName);
251             mCameraManager.injectCamera(packageName, cameraId, /* externalCamId */ "",
252                     mContext.getMainExecutor(),
253                     new CameraInjectionSession.InjectionStatusCallback() {
254                         @Override
255                         public void onInjectionSucceeded(
256                                 @NonNull CameraInjectionSession session) {
257                             CameraAccessController.this.onInjectionSucceeded(cameraId, packageName,
258                                     session);
259                         }
260 
261                         @Override
262                         public void onInjectionError(@NonNull int errorCode) {
263                             CameraAccessController.this.onInjectionError(cameraId, packageName,
264                                     errorCode);
265                         }
266                     });
267         } catch (CameraAccessException e) {
268             Slog.e(TAG,
269                     "Failed to injectCamera for cameraId:" + cameraId + " package:" + packageName,
270                     e);
271         }
272     }
273 
onInjectionSucceeded(String cameraId, String packageName, @NonNull CameraInjectionSession session)274     private void onInjectionSucceeded(String cameraId, String packageName,
275             @NonNull CameraInjectionSession session) {
276         synchronized (mLock) {
277             InjectionSessionData data = mPackageToSessionData.get(packageName);
278             if (data == null) {
279                 Slog.e(TAG, "onInjectionSucceeded didn't find expected entry for package "
280                         + packageName);
281                 session.close();
282                 return;
283             }
284             CameraInjectionSession existingSession = data.cameraIdToSession.put(cameraId, session);
285             if (existingSession != null) {
286                 Slog.e(TAG, "onInjectionSucceeded found unexpected existing session for camera "
287                         + cameraId);
288                 existingSession.close();
289             }
290         }
291     }
292 
onInjectionError(String cameraId, String packageName, @NonNull int errorCode)293     private void onInjectionError(String cameraId, String packageName, @NonNull int errorCode) {
294         if (errorCode != ERROR_INJECTION_UNSUPPORTED) {
295             // ERROR_INJECTION_UNSUPPORTED means that there wasn't an external camera to map to the
296             // internal camera, which is expected when using the injection interface as we are in
297             // this class to simply block camera access. Any other error is unexpected.
298             Slog.e(TAG, "Unexpected injection error code:" + errorCode + " for camera:" + cameraId
299                     + " and package:" + packageName);
300             return;
301         }
302         synchronized (mLock) {
303             InjectionSessionData data = mPackageToSessionData.get(packageName);
304             if (data != null) {
305                 mBlockedCallback.onCameraAccessBlocked(data.appUid);
306             }
307         }
308     }
309 
queryUidFromPackageName(int userId, String packageName)310     private int queryUidFromPackageName(int userId, String packageName) {
311         try {
312             final ApplicationInfo ainfo =
313                     mPackageManager.getApplicationInfoAsUser(packageName,
314                         PackageManager.GET_ACTIVITIES, userId);
315             return ainfo.uid;
316         } catch (PackageManager.NameNotFoundException e) {
317             Slog.w(TAG, "queryUidFromPackageName - unknown package " + packageName, e);
318             return Process.INVALID_UID;
319         }
320     }
321 }
322