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